Skip to content

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

FormMay doMust not do
Synchronous ListenerA 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 ListenerReconstruct, 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}.phpGenerated synchronous Listener boundary.

php
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.

php
$sendReceipt->execute($event->orderId); 

Context: ✅ Correct handoff — Queue a bounded reaction when work needs a worker boundary.

php
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}.phpGenerated queued after-commit Listener requirements.

php
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.