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}.php — Generated 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}.php — Generated 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.