Master Laravel's event system to decouple logic, improve testability, and scale your application with event listeners, subscribers, and broadcasting.
Laravel’s event system allows you to build highly decoupled applications where logic can be neatly separated, extended, and scaled. This approach shines when triggering multiple reactions from a single action - like sending emails, logging activity, and queuing background jobs after a purchase.
What This Article Needed (And Now Has)
Events are not just “fire and forget”. They are a tool for decoupling and for scaling workflows without turning controllers into orchestration scripts.
This post now clarifies:
- When to use events vs direct method calls
- How queued listeners affect user-perceived performance
- How to avoid “event soup” where nothing is traceable
- How to keep events domain-focused and stable
Fact Checks and Clarifications
- Broadcasting is a specific concern. Not every domain event should implement
ShouldBroadcast. Keep domain events separate from “UI update” events unless the broadcast is the core requirement. - In production, caching event discovery manifests is a valid optimisation and is commonly done as part of deployment (via
event:cacheor the broaderoptimizecommand).
Design Rationale and Trade-offs
Events make change cheaper
The real win is future change. You can add a new reaction (analytics, notifications, auditing) without editing core business flows.
But events can hide complexity
If everything is an event, the execution path becomes hard to follow. The fix is conventions:
- Name events after business facts (
OrderPlaced,SubscriptionCancelled) - Keep payloads small and purposeful
- Log and monitor listener failures
Queueing is part of the architecture
If listeners call external services, queue them. Your application stays responsive and failures can be retried without blocking the user.
Practical Patterns Worth Adding
- Use queued listeners for anything that touches email, third-party APIs, PDFs, or reports
- Prefer a small number of meaningful events over dozens of “implementation” events
- Consider an outbox pattern if you require guaranteed delivery across systems
FAQ
Should I dispatch events in controllers or services?
Prefer services or domain actions. Controllers should be thin.
How do I test event-driven flows?
Fake events for unit-level tests, and run real listeners in a smaller number of end-to-end feature tests.
Is this worth it for small apps?
Sometimes. If the app has payments, integrations, or auditing needs, events pay off early.
Key Takeaways
- Events reduce coupling and make extension easier.
- Queue heavy listeners and monitor failures.
- Avoid “event soup” by keeping events domain-focused.
Why Use Events?
Event-driven design:
- Decouples logic – keeps core actions lean
- Improves testability – listeners can be tested in isolation
- Enhances scalability – listeners can run synchronously or via queues
- Encourages SRP – each listener handles a single job
Basic Event + 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\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderPlaced
{
use Dispatchable, InteractsWithSockets, 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));
}
}
Register in EventServiceProvider.php:
protected $listen = [
OrderPlaced::class => [
SendOrderConfirmation::class,
LogOrderActivity::class,
QueueAnalyticsEvent::class,
],
];
Dispatching Events
Anywhere in your app:
OrderPlaced::dispatch($order);
Or use the helper:
event(new OrderPlaced($order));
Listener Queues for Performance
Use queued listeners:
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. Monitor it with Horizon.
Event Subscribers for Clean Grouping
Subscribers let you group multiple 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 in EventServiceProvider.php:
protected $subscribe = [
UserEventSubscriber::class,
];
Broadcasting Events
If you need real-time UI updates, broadcasting is a separate concern from your domain event. Keep it explicit and only broadcast what you genuinely need.
Broadcast events via Pusher, Ably, or Redis:
class OrderPlaced
{
public function broadcastOn()
{
return new PrivateChannel('orders.'.$this->order->id);
}
}
Enable broadcasting in config/broadcasting.php, set up a frontend listener with Laravel Echo:
Echo.private(`orders.${orderId}`)
.listen('OrderPlaced', (e) => {
console.log('New order broadcast received', e);
});
Best Practices
- Use event names that reflect domain actions (e.g.
OrderShipped,InvoiceGenerated) - Keep listeners focused and lean
- Queue heavy work like notifications, analytics, or third-party calls
- Test events and listeners in isolation
- Monitor your queues with Horizon or Telescope
Real-World Application
In the high-performance Laravel ecommerce project, we used events to:
- Queue order processing steps
- Broadcast inventory updates
- Log user activity asynchronously
Related Posts
Looking for Laravel support? I help businesses across Chester and the North West build and maintain Laravel applications. Learn more about my Laravel services or get in touch.