Laravel Mastery: Event-driven architecture

Laravel events and listeners that keep side jobs out of your controllers: queued listeners, subscribers and broadcasting, with a trading platform example.

Updated 7 min read

Laravel’s events let one action trigger several reactions, without the code that does the action knowing about them. After a purchase, one event can send the email, log the activity and queue a sync, each in its own listener. The controller stays short, and you can add a reaction without touching it.

Why use events?

Event-driven design:

  • Decouples logic: the core action stays short
  • Improves testability: listeners can be tested in isolation
  • Moves slow work off the request: listeners can run synchronously or via queues
  • Encourages single responsibility: each listener handles one job

An event and a listener

Generate both:

php artisan make:event OrderPlaced
php artisan make:listener SendOrderConfirmation --event=OrderPlaced

Event

<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderPlaced
{
    use Dispatchable, SerializesModels;

    public function __construct(public Order $order)
    {
    }
}

Listener

class SendOrderConfirmation
{
    public function handle(OrderPlaced $event)
    {
        Mail::to($event->order->user)->send(new OrderConfirmation($event->order));
    }
}

In Laravel 11 and later, event discovery registers this for you. Laravel scans app/Listeners and links each listener to the event type-hinted in its handle() method, so there’s nothing else to wire up.

To register a listener by hand, for example one that lives outside app/Listeners, use Event::listen in the boot() method of AppServiceProvider:

use Illuminate\Support\Facades\Event;

Event::listen(OrderPlaced::class, SendOrderConfirmation::class);

Older applications list events and listeners in the $listen array of EventServiceProvider instead.

Dispatching events

Anywhere in your app:

OrderPlaced::dispatch($order);

Or use the helper:

event(new OrderPlaced($order));

Queued listeners

Implement ShouldQueue and the listener runs on a queue worker instead of during the request:

use Illuminate\Contracts\Queue\ShouldQueue;

class QueueAnalyticsEvent implements ShouldQueue
{
    public function handle(OrderPlaced $event)
    {
        Analytics::track('order_placed', [
            'order_id' => $event->order->id,
            'user_id' => $event->order->user_id,
        ]);
    }
}

Queue config lives in config/queue.php. On Redis queues, watch the workers with Horizon.

Grouping listeners in a subscriber

A subscriber holds several listeners in one class:

class UserEventSubscriber
{
    public function onLogin($event)
    {
        // log or notify
    }

    public function onLogout($event)
    {
        // track session end
    }

    public function subscribe(Dispatcher $events)
    {
        $events->listen(Login::class, [self::class, 'onLogin']);
        $events->listen(Logout::class, [self::class, 'onLogout']);
    }
}

Register it in the boot() method of AppServiceProvider:

Event::subscribe(UserEventSubscriber::class);

Broadcasting events

For live updates in the browser, keep broadcasting separate from your domain events, and broadcast only what the page needs.

A broadcast event implements ShouldBroadcast. Without it, Laravel never sends it to the browser. Give it its own class, and send only the fields the UI needs:

use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class OrderStatusUpdated implements ShouldBroadcast
{
    public function __construct(public int $orderId, public string $status)
    {
    }

    public function broadcastOn(): PrivateChannel
    {
        return new PrivateChannel('orders.'.$this->orderId);
    }
}

Run php artisan install:broadcasting to set up broadcasting and Laravel Echo, then authorise the private channel in routes/channels.php so users only hear about their own orders. Listen on the frontend:

Echo.private(`orders.${orderId}`)
    .listen('OrderStatusUpdated', (e) => {
        console.log('Order status changed', e.status);
    });

Habits that help

  • Use event names that reflect domain actions such as OrderShipped or InvoiceGenerated
  • Give each listener one job
  • Queue heavy work like notifications, analytics, or third-party calls
  • Test events and listeners in isolation
  • Monitor your queues with Horizon or Telescope

A real example

On a Laravel trading platform, a queue-based engine matched orders with rollback-safe database transactions, and Redis broadcasting through Laravel Echo pushed live prices and order updates to every user.


Events are how I keep the follow-on work of an action, like an email or a sync, out of its controller when I build a Laravel application.

Published , updated .

Part 4 of Laravel Mastery.

Related posts