php 120 lines · 4 tabs

Fan Out an Order Placed Domain Event to Multiple Queued Laravel Listeners

Shared by codesnips Aug 2026
4 tabs
<?php

namespace App\Providers;

use App\Events\OrderPlaced;
use App\Listeners\DecrementInventory;
use App\Listeners\RecordOrderAnalytics;
use App\Listeners\SendOrderConfirmation;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        OrderPlaced::class => [
            SendOrderConfirmation::class,
            DecrementInventory::class,
            RecordOrderAnalytics::class,
        ],
    ];

    public function shouldDiscoverEvents(): bool
    {
        return false;
    }
}
4 files · php Explain with highlit

This snippet shows how a single domain event on order placement can fan out to several independent, asynchronously processed listeners in Laravel. The pattern decouples the write path (persisting the order) from the side effects (email, inventory, analytics) so the HTTP request stays fast and each side effect can fail and retry in isolation.

In OrderPlaced event, the event is a plain value object carrying the Order model. It uses Dispatchable, SerializesModels, and InteractsWithSockets, and exposes a read-only $order on construction. SerializesModels matters because queued listeners are serialized to the queue backend; rather than shipping the whole model, only its identifier is stored and the model is re-resolved from the database when the job runs, which keeps payloads small and avoids stale state.

EventServiceProvider wires the fan-out declaratively via the $listen map: one event key, an array of listener classes. Registering multiple listeners under OrderPlaced::class is what makes this a fan-out — the framework invokes each one, and because they implement ShouldQueue, each is pushed as its own queue job with independent retries. SendOrderConfirmation listener demonstrates the queued listener contract: implementing ShouldQueue moves work off the request thread, $queue = 'mail' and $tries/$backoff tune where and how aggressively it retries, and viaConnection can route to a specific broker.

The listener's handle method is written to be idempotent using Cache::add keyed by order id, because at-least-once delivery means a job can legitimately run twice after a transient failure and a re-dispatch. Without this guard, a retry could send a duplicate confirmation email. failed receives the throwable after the final attempt is exhausted, giving a place to alert or record a dead-letter record.

OrdersController triggers the whole flow: it persists the order inside a DB::transaction, then calls OrderPlaced::dispatch only after the commit closure returns, so listeners never observe an order that was rolled back. The trade-off of this approach is eventual consistency — side effects complete slightly after the response — in exchange for a responsive endpoint and resilient, individually retryable side effects. A developer reaches for this when one user action must trigger several unrelated jobs that should not block each other.


Related snips

Share this code

Here's the card — post it anywhere.

Fan Out an Order Placed Domain Event to Multiple Queued Laravel Listeners — share card
Link copied