Choose a Pulsar Listener reaction
A synchronous Listener stays bounded; a queued-after-commit Listener may enter one UseCase. Neither form owns a transaction, supplies durable delivery, or removes the need for authorization, tenant reconstruction, idempotency, and reentrancy protection.
Choose the reaction mode
| Form | May do | Must not do |
|---|---|---|
| Synchronous Listener | A small Contract side effect, Notification or Job handoff, or read-only invariant check. | Call a UseCase, start a transaction, hide branching workflow, or rely on rollback or crash survival. |
| Queued after-commit Listener | Reconstruct, authorize, protect an idempotent/reentrant reaction, then call at most one UseCase. | Omit either queue interface, carry a live Model, own a transaction, or prescribe worker operations. |
Keep synchronous reactions bounded
The synchronous stub makes its non-HTTP authorization explicit. It is suitable only when the reaction can tolerate being in the current process and transaction visibility window.
app/Pulsar/Domain/{Domain}/Listeners/{Name}.php — Generated synchronous Listener boundary.
final class SendReceipt
{
public function handle(OrderPlaced $event): void
{
Gate::authorize('replace-with-domain-ability', $event);
// Never call a UseCase, start a transaction, or assume durability.
}
}Context: ❌ Prohibited synchronous workflow — A Listener must not create a second transaction owner.
$sendReceipt->execute($event->orderId); Context: ✅ Correct handoff — Queue a bounded reaction when work needs a worker boundary.
SendReceipt::dispatch($event->orderId)->afterCommit(); Enter one workflow from a queued Listener
The queued generated form implements ShouldQueue and ShouldQueueAfterCommit, uses Queueable, and declares three tries. It is the only Listener form that may resolve and call one UseCase; that UseCase owns the transaction and business branching.
app/Pulsar/Domain/{Domain}/Listeners/{Name}.php — Generated queued after-commit Listener requirements.
class SendReceipt implements ShouldQueue, ShouldQueueAfterCommit
{
use Queueable;
public int $tries = 3;
public function handle(OrderPlaced $event): void
{
// Re-establish actor/tenant context, then authorize.
// Make every side effect or one UseCase call idempotent.
}
}Reconstruct before acting
Carry IDs, scalars, DTOs, or Value Objects. A worker must reconstruct actor and tenant data, reject a missing actor or tenant, authorize the named ability with an explicit subject or value, and re-fetch tenant-scoped state before it calls the one UseCase. This is Adapter work, not a Context API, discovery rule, or tenancy implementation.
Handle retry and reentrancy
Queued delivery is at-least-once. Keep the same Event identity or idempotency key through retries, make the effect safe for repeated handling, and use a reentrancy guard to stop a reaction loop. A guard is not authorization, tenant isolation, an invariant, or exactly-once delivery; its persistence choice remains application-owned. A terminal failed() path may report failure without launching a replacement workflow. This page intentionally does not prescribe retry infrastructure or Listener discovery.