Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Events and Listeners Real Use Cases

By Kokil Thapa | Last reviewed: September 2026

Laravel Events and Listeners Real Use Cases matter the moment a controller starts doing too much: sending mail, writing audit rows, calling payment APIs, and updating search indexes in one method. That pattern works in a tutorial, but on a production Laravel enterprise application it becomes brittle—every new requirement edits the same action, tests get slow, and deployments feel risky. Events and listeners give you a clean contract: the core business action fires one event; independent listeners handle side effects. This guide walks through patterns I use on live Laravel 12 and 13 projects—PHP 8.3+, queued listeners, and decisions about when not to reach for events at all.

What are Laravel events and listeners, and when should you use them?

An event is a plain PHP class—often a DTO carrying the data listeners need. A listener is a class with a handle() method that reacts to that event. Laravel's event dispatcher wires them together through EventServiceProvider or auto-discovery.

Use events when:

  • One action triggers multiple independent reactions (email + SMS + analytics + webhook).
  • Reactions may be added later without touching the original controller or service.
  • You want to queue slow I/O (mail, HTTP calls, PDF generation) away from the request cycle.
  • Third-party packages already emit events you can hook into (Illuminate\Auth\Events\Registered, Cashier webhooks, etc.).

Skip events when the logic is a single, synchronous step tightly bound to the transaction—use a service method or an Eloquent observer instead. I've seen teams create events for every model save; that adds indirection without benefit. Save events for business milestones: order paid, booking confirmed, document approved.

Laravel Event Dispatch FlowControlleror ServiceDomain EventOrderPlacedEventDispatcherSendReceiptListener (queued)UpdateInventoryListener (sync)NotifyWebhookListener (queued)Core action completes firstListeners run after — sync or via queue worker
Laravel Events and Listeners Real Use Cases: one domain event fans out to independent listeners without bloating the controller.

Laravel 13.x (PHP 8.3 minimum) and Laravel 12 (PHP 8.2+) share the same event API. If you are still on Laravel 11, note it reached end of life in March 2026—plan an upgrade using a staged migration approach described in our Laravel 12 migration guide.

How do you register Laravel events and listeners in a production app?

Start with explicit registration while the team learns the pattern; switch to auto-discovery once conventions are stable.

Step 1: Create the event and listeners

php artisan make:event Orders/OrderPlaced
php artisan make:listener Orders/SendOrderReceipt --event=Orders/OrderPlaced
php artisan make:listener Orders/NotifyFulfillmentTeam --event=Orders/OrderPlaced --queued

Your event carries the domain object—not raw request input:

<?php

namespace App\Events\Orders;

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

class OrderPlaced
{
    use Dispatchable, SerializesModels;

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

Step 2: Register in EventServiceProvider

protected $listen = [
    \App\Events\Orders\OrderPlaced::class => [
        \App\Listeners\Orders\SendOrderReceipt::class,
        \App\Listeners\Orders\NotifyFulfillmentTeam::class,
        \App\Listeners\Orders\PushOrderToAnalytics::class,
    ],
];

Laravel 11+ moved provider configuration into bootstrap/app.php, but EventServiceProvider still works when you register it. Alternatively, enable listener discovery:

public function shouldDiscoverEvents(): bool
{
    return true;
}

Discovery scans app/Listeners and maps listeners whose type-hinted handle() parameter matches the event class. That keeps registration DRY on larger codebases aligned with modern Laravel architecture.

Step 3: Dispatch from a service, not a fat controller

<?php

namespace App\Services;

use App\Events\Orders\OrderPlaced;
use App\Models\Order;
use Illuminate\Support\Facades\DB;

class CheckoutService
{
    public function placeOrder(array $cartData): Order
    {
        return DB::transaction(function () use ($cartData) {
            $order = Order::create([/* validated data */]);
            $order->items()->createMany($cartData['items']);

            OrderPlaced::dispatch($order);

            return $order;
        });
    }
}

Dispatch inside the transaction when listeners must see committed data, or after commit when listeners call external systems that should not run if the DB rolls back. Laravel provides DB::afterCommit() wrapping:

OrderPlaced::dispatch($order)->afterCommit();

On payment integrations—Stripe, Khalti, eSewa—I've burned hours debugging listeners that fired before the transaction committed. Default to afterCommit() for anything that sends money confirmations or external webhooks. Our Khalti integration guide covers callback timing in more detail.

What are real Laravel events and listeners use cases from production apps?

These patterns come from eCommerce, booking, and legal-tech portals I've shipped—not abstract demos.

1. Order placed: email, inventory, and analytics

On a Laravel eCommerce build similar to Nepal Gift Card, checkout dispatches OrderPlaced. Listeners handle:

  1. Customer receipt email — queued; uses the order's eager-loaded relations.
  2. Inventory decrement — synchronous inside the same request if stock must be reserved immediately.
  3. Analytics pixel / server-side event — queued; failures should not block checkout.
<?php

namespace App\Listeners\Orders;

use App\Events\Orders\OrderPlaced;
use App\Mail\OrderReceipt;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;

class SendOrderReceipt implements ShouldQueue
{
    public int $tries = 3;

    public function handle(OrderPlaced $event): void
    {
        Mail::to($event->order->customer_email)
            ->send(new OrderReceipt($event->order));
    }
}
OrderPlaced Event SequenceCheckoutDB TxnOrderPlacedHTTP 302create orderdispatch eventreturn fastQueue worker (async listeners)SendReceiptAnalyticsWebhookUser sees thank-you page while workers process side effects
Real eCommerce Laravel Events and Listeners Real Use Cases: checkout returns quickly while queued listeners handle mail and integrations.

2. Booking confirmed: SMS, calendar, and supplier notification

Trekking and appointment systems—like booking flows on Adventure Third Pole Trek—fire BookingConfirmed after payment verification. Listeners send SMS via a gateway, email the itinerary PDF, and POST to a supplier's webhook. Each channel is isolated; disabling SMS for a staging environment means skipping one listener, not commenting out controller code.

3. Document uploaded: virus scan, thumbnail, and audit trail

Legal-tech client portals need an immutable audit log when a user uploads a contract or affidavit. On portals in the same family as Mijar Law Associates, DocumentUploaded triggers:

  • A queued listener that pushes the file to a scanning service.
  • A listener that generates a preview thumbnail via Spatie Media Library conversions.
  • A synchronous listener that writes an audit_logs row with user ID, IP, and document hash.

Audit logging stays synchronous so it exists before the user navigates away. Scanning can lag seconds without UX impact.

4. User registered: welcome email and CRM sync

Laravel ships Registered out of the box. Hook your own listener alongside SendEmailVerificationNotification:

use Illuminate\Auth\Events\Registered;

protected $listen = [
    Registered::class => [
        SendEmailVerificationNotification::class,
        SyncUserToCrm::class,
        AssignDefaultRole::class,
    ],
];

Keep CRM sync queued. A slow HubSpot or Zoho API must never delay registration response time.

5. Payment captured: ledger entry and fulfilment

Payment gateways return asynchronously. When your webhook controller verifies a Khalti or Stripe payload and marks an order paid, dispatch PaymentCaptured rather than duplicating fulfilment logic in the webhook and the admin panel. One event, one set of listeners—critical for Laravel payment integrations where the same order can be confirmed from multiple entry points.

6. Lead form submitted: notifications and spam scoring

Law-firm lead capture on sites like Court Marriage In Nepal dispatches LeadSubmitted. Listeners notify staff on Slack or email, persist UTM parameters, and optionally call a spam-scoring API. The form controller validates and creates the lead—nothing else.

How should you queue Laravel listeners for slow side effects?

Implement Illuminate\Contracts\Queue\ShouldQueue on any listener that touches the network, filesystem, or third-party APIs. Your queue driver in production should be Redis 8.x (or database queue for low-traffic sites).

<?php

namespace App\Listeners\Orders;

use App\Events\Orders\OrderPlaced;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class PushOrderToAnalytics implements ShouldQueue
{
    use InteractsWithQueue;

    public string $queue = 'analytics';
    public int $tries = 5;
    public array $backoff = [30, 60, 120];

    public function handle(OrderPlaced $event): void
    {
        /* HTTP POST to analytics endpoint */
    }

    public function failed(OrderPlaced $event, \Throwable $e): void
    {
        report($e);
        /* optional: dead-letter notification */
    }
}

Run dedicated workers per queue so a stuck analytics job does not block receipt emails:

php artisan queue:work redis --queue=default,mail,analytics --tries=3
Queued Listener PipelineEventdispatchRedisqueueWorkerprocessListenerhandle()Mail / SMSexternal APIWebhooksretry + backoffSearch indexMeilisearchFailed jobs: failed() hook + Horizon monitoringSupervisor keeps workers alive on Ubuntu production servers
Queue Laravel listeners with Redis so HTTP requests finish before mail, webhooks, and search indexing run.

Serializing Eloquent models in queued listeners uses SerializesModels. Laravel re-fetches the model when the job runs—if it was deleted, the job is discarded. Pass IDs explicitly when you need custom handling:

public function __construct(public int $orderId) {}

public function handle(): void
{
    $order = Order::findOrFail($this->orderId);
}

For long-running listener chains, consider whether you actually need event sourcing—most CRUD apps do not. Events plus queues cover 90% of decoupling needs without an event store.

Events vs observers vs jobs: which pattern fits your Laravel app?

Teams often confuse four overlapping tools. Here is how I choose on production codebases.

PatternBest forTriggerMultiple handlers?Queue built-in?
Events + ListenersBusiness milestones with several independent reactionsExplicit dispatch()YesPer-listener via ShouldQueue
Eloquent ObserversModel lifecycle hooks (creating, saving, deleted)Automatic on model eventsOne observer class per modelObserver can implement ShouldQueue (Laravel 11+)
JobsSingle background task with clear input/outputExplicit dispatch()No (one job, one handle)Yes, by design
NotificationsUser-facing alerts across mail/SMS/Slack/database$user->notify()Channels within one notificationVia ShouldQueue

Rule of thumb: if the trigger is "user completed checkout", use an event. If the trigger is "every time updated_at changes on Article", use an observer. If the trigger is "generate this one PDF tonight", dispatch a job. Notifications sit alongside listeners—you can call $order->user->notify(new OrderShipped($order)) from inside a listener.

Which Pattern to Use?New side effect neededmodel save?yesObservernoBusiness action?checkout, uploadmultiple?yesEvent + ListenersnoJob
Decision tree for Laravel Events and Listeners Real Use Cases versus observers and standalone jobs.

Testing events and listeners

Use Event::fake() in feature tests to assert an event was dispatched without running listeners:

Event::fake([OrderPlaced::class]);

$this->post('/checkout', $payload);

Event::assertDispatched(OrderPlaced::class, function ($event) {
    return $event->order->total === 1500;
});

Test listeners in isolation by instantiating the event with a factory-built model and calling handle() directly. That keeps unit tests fast compared to full HTTP cycles. When building Laravel API endpoints that dispatch events, fake events in API tests the same way.

Subscriber classes for cross-cutting concerns

When one class listens to many events—logging every auth event, for example—use an event subscriber:

<?php

namespace App\Listeners;

use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;

class AuthActivitySubscriber
{
    public function handleLogin(Login $event): void { /* log */ }
    public function handleLogout(Logout $event): void { /* log */ }

    public function subscribe(Dispatcher $events): array
    {
        return [
            Login::class => 'handleLogin',
            Logout::class => 'handleLogout',
        ];
    }
}

Register subscribers in EventServiceProvider::$subscribe. Subscribers reduce mapping noise when you have a dozen auth or billing events.

Broadcasting and real-time UI

Events can implement ShouldBroadcast to push updates over websockets. That overlaps with dedicated broadcasting setup—see Laravel Broadcasting with Reverb and real-time features with websockets and Redis. Keep domain events separate from broadcast events when payloads differ; a listener can dispatch a broadcast-specific event if needed.

Common production mistakes

  • Fat listeners — a listener that grows to 200 lines should become a service called by a thin listener.
  • Recursive events — a listener that updates a model whose observer dispatches the same event causes infinite loops. Use $model->saveQuietly() or guard with a flag.
  • Missing queue workers — queued listeners silently pile up in Redis if Supervisor is not running. Monitor queue depth after deploy.
  • Passing huge payloads — serialize IDs, not entire object graphs. Use the JSON formatter tool to inspect queue payload size during debugging.
  • Synchronous external calls — calling payment verify or SMS APIs in a non-queued listener adds seconds to every request.

Authorization still belongs in policies and gates—not listeners. Listeners assume the action already passed validation and auth checks. See our Laravel policies and gates guide for where those checks live.

For SEO-heavy Laravel sites, sitemap regeneration and cache busting also fit the event model: dispatch ArticlePublished and let a listener ping your search index and warm CDN caches—details in SEO for Laravel sites.

If you expose events to external consumers, pair internal domain events with a documented public API rather than leaking Eloquent models across boundaries—patterns covered in building RESTful APIs with Laravel and API development services.

On shared hosting without Redis, database queues work for low-volume mail listeners. Move to Redis 8.x and dedicated workers once daily order volume or document uploads exceed what a single cron-driven worker can drain in a minute. Linux server administration and proper Supervisor config matter as much as the PHP code.

Need structured logging of every dispatched event during local development? Temporarily register a wildcard listener in AppServiceProvider that logs class names—remove it before production. For payload inspection, the regex tester helps parse log lines when debugging listener failures.

Complex read models and audit history may eventually push you toward CQRS or event sourcing, but that is a separate architectural step—start with simple events and add complexity only when replay or temporal queries become a business requirement. Our CQRS pattern guide explains that threshold.

Official references: the Laravel events documentation covers registration and discovery, and the queues documentation explains ShouldQueue, retries, and failed-job handling. For PHP version requirements on Laravel 13, see the Laravel 13 release notes.

Key Takeaways

  • Fire domain events at business milestones—order placed, booking confirmed, document uploaded—not at every Eloquent save.
  • Queue any listener that sends mail, calls HTTP APIs, or generates files; use afterCommit() when listeners depend on committed DB state.
  • Keep controllers and services thin: validate, persist, dispatch one event, return the response.
  • Choose observers for model lifecycle hooks, jobs for single background tasks, and events when multiple independent reactions are needed.
  • Test with Event::fake() at the feature level and direct handle() calls at the unit level.
  • Monitor queue workers in production—queued listeners do nothing useful if Redis jobs sit unprocessed.

People Also Ask

Can Laravel listeners run synchronously and asynchronously in the same app?

Yes. Each listener class decides independently. Implement ShouldQueue on slow listeners; leave fast, critical listeners synchronous. Laravel runs synchronous listeners during the dispatch call and pushes queued ones to your configured driver.

What is the difference between Laravel events and webhooks?

Events are internal application signals. Webhooks are outbound HTTP calls to external systems. A listener often sends a webhook, but the event itself stays inside your Laravel app boundary.

Should I use Laravel events for logging and auditing?

For significant business actions, yes—an audit listener writing to an audit_logs table keeps a clear trail. For debug logging during development, use Laravel's log facade directly; not every log line needs an event.

Do Laravel packages use events I can listen to?

Many do. Cashier fires subscription events, Passport emits token events, and the framework itself exposes auth, mail, and notification hooks. Check the package docs for event class names before writing custom hooks.

Ship decoupled Laravel apps with confidence

Laravel Events and Listeners Real Use Cases come down to one habit: finish the core transaction, dispatch one well-named event, and let focused listeners handle everything else. That structure scales from a law-firm lead form to a multi-currency gift-card checkout without rewriting checkout every time marketing asks for another integration. If your controllers are already crowded with mail and API calls, refactoring to events is one of the highest-return architecture changes you can make on an existing Laravel 12 or 13 codebase.

Need help untangling a monolithic controller or wiring queued listeners on production infrastructure? Contact us to discuss your project, or browse the portfolio for examples of Laravel systems already running this pattern in Nepal and abroad. For ongoing work after launch, see support and maintenance options and related posts on the blog, including why Laravel fits Nepali businesses and about the developer behind these builds.

Frequently Asked Questions

An event is a plain PHP class, often a DTO carrying domain data. A listener is a class with a handle() method that reacts to that event. Laravel's dispatcher wires them through EventServiceProvider or auto-discovery.

Use them when one business action triggers multiple independent reactions—email, SMS, analytics, webhooks—and when reactions may be added later without editing the original controller or service. Queue slow I/O like mail, HTTP calls, and PDF generation away from the request cycle. Hook into framework events such as Illuminate\Auth\Events\Registered or Cashier webhooks. Skip events when logic is a single synchronous step tightly bound to a transaction; use a service method or Eloquent observer instead. Save events for business milestones like order paid, booking confirmed, or document approved—not every model save.

Create classes with php artisan make:event and php artisan make:listener, optionally with --queued. Register explicitly in EventServiceProvider protected $listen array while the team learns the pattern. Laravel 11+ also supports configuration in bootstrap/app.php, but EventServiceProvider still works when registered. Enable shouldDiscoverEvents() once conventions are stable; discovery scans app/Listeners and maps listeners whose type-hinted handle() parameter matches the event class. Dispatch from a service such as CheckoutService, not a fat controller, after the core business action succeeds inside or after your database transaction.

OrderPlaced fans out to receipt email, inventory decrement, and analytics. BookingConfirmed triggers SMS, itinerary PDF email, and supplier webhooks after payment verification. DocumentUploaded starts virus scanning, thumbnail generation via Spatie Media Library, and synchronous audit logging on legal-tech portals. Registered hooks welcome email, CRM sync, and default role assignment alongside Laravel's built-in verification listener. PaymentCaptured unifies fulfilment when Stripe, Khalti, or eSewa webhooks and admin actions mark the same order paid. LeadSubmitted handles staff notifications, UTM persistence, and spam scoring on law-firm lead forms.

Yes—queue any listener that touches the network, filesystem, or third-party APIs by implementing ShouldQueue.

Dispatch inside the transaction when listeners must see data that has not yet committed—rare for external systems. Dispatch after commit when listeners call payment gateways, send money confirmations, or fire webhooks that should not run if the database rolls back. Laravel provides OrderPlaced::dispatch($order)->afterCommit() for this. On Stripe, Khalti, and eSewa integrations, listeners firing before commit cause hard-to-debug failures where external systems see orders that never persisted. Default to afterCommit() for anything touching external confirmation flows.

Events plus listeners fit business milestones with several independent handlers triggered by explicit dispatch(). Observers hook automatic model lifecycle events—creating, saving, deleted—with one observer class per model; Laravel 11+ observers can implement ShouldQueue. Jobs suit a single background task with clear input and output, dispatched explicitly with one handle method. Notifications deliver user-facing alerts across mail, SMS, Slack, and database channels via $user->notify(). Rule of thumb: user completed checkout means an event; every updated_at change on Article means an observer; generate one PDF tonight means a job. Notifications often run inside listeners.

Implement Illuminate\Contracts\Queue\ShouldQueue on listeners touching the network or third-party APIs. Use Redis 8.x as the queue driver in production, or database queue on low-traffic sites. Set per-listener queue names, retry counts, and backoff arrays. Run dedicated workers per queue so stuck analytics jobs do not block receipt emails: php artisan queue:work redis --queue=default,mail,analytics --tries=3. Implement failed() to report errors and optionally notify operators. SerializesModels re-fetches Eloquent models when the job runs; pass IDs explicitly when you need custom handling for deleted records.

Use Event::fake() in feature tests to assert an event was dispatched without running listeners: fake OrderPlaced, perform the HTTP or API action, then Event::assertDispatched with a callback checking payload such as order total. Test listeners in isolation by instantiating the event with a factory-built model and calling handle() directly. That keeps unit tests fast compared to full HTTP cycles. Apply the same Event::fake() pattern when building Laravel API endpoints that dispatch events.

Skip events when the logic is one synchronous step tightly bound to the transaction—use a service method or Eloquent observer instead. Avoid creating events for every model save; that adds indirection without benefit. Do not put authorization in listeners; policies and gates validate before the action fires. Listeners assume validation and auth already passed. Most CRUD apps do not need event sourcing; events plus queues cover most decoupling without an event store.

Redis 8.x is the recommended production queue driver for queued listeners on Laravel 12 and 13 projects. It handles mail, webhooks, analytics, and search-index updates reliably under load. Database queue works for low-traffic sites but becomes a bottleneck as listener volume grows. Regardless of driver, run Supervisor-managed queue workers and monitor queue depth after every deploy—queued listeners pile up silently when workers are missing.

When your webhook controller verifies a Khalti, Stripe, or eSewa payload and marks an order paid, dispatch PaymentCaptured instead of duplicating fulfilment logic in the webhook handler and admin panel. One event and one set of listeners ensures the same order confirmed from multiple entry points triggers identical side effects. Wrap dispatch in afterCommit() so external confirmations never fire for rolled-back transactions. Keep webhook verification synchronous in the controller; queue downstream mail and fulfilment listeners.

Fat listeners that grow to hundreds of lines should become services called by thin listeners. Recursive events occur when a listener updates a model whose observer re-dispatches the same event—use saveQuietly() or guard flags. Missing queue workers let jobs pile up in Redis unnoticed after deploy. Passing huge serialized object graphs bloats queue payloads; pass IDs instead. Synchronous external calls to payment verify or SMS APIs in non-queued listeners add seconds to every request. Authorization belongs in policies, not listeners.

Event subscribers are single classes that listen to many events—logging every auth event, for example. They define handle methods per event plus a subscribe() method returning an event-to-method map. Register them in EventServiceProvider::$subscribe. Subscribers reduce mapping noise when you have a dozen auth or billing events instead of listing each pair in the $listen array. Use them for cross-cutting concerns like activity logging across Login, Logout, and related auth events.

No—for checkout, use an explicit domain event like OrderPlaced dispatched from CheckoutService after the transaction succeeds. Observers fire on model lifecycle hooks such as creating or saving, which suits automatic reactions to every record change, not a deliberate business milestone. Checkout involves validated cart data, payment timing, and multiple independent side effects—email, inventory, analytics—that observers cannot cleanly orchestrate. If a listener updates a model, avoid observer-triggered recursive events by using saveQuietly() or guard flags.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: