Your Eloquent listener seemed perfect: wire it up to the Order model's saved event, send a confirmation email when an order is saved, and you're done. It works in the demo. Then production hits, and a support ticket lands. A customer got two confirmation emails for the same purchase. Another one got a refund receipt without a cancellation notice. You check your logs and find nothing wrong. The emails went out. The code looks fine. The problem is subtle, and it lives in your architecture.
Why This Matters Today (July 2026)
In 2026, code that works for a weekend project but falls apart in production is more expensive than ever. Your team expects the app to scale. Your customers expect reliability. Framework hooks are convenient, but convenience often comes at the cost of control. As your application grows beyond a proof-of-concept, the decisions you make about how to handle side effects—like sending emails—determine whether your app stays maintainable or becomes a maze of implicit dependencies.
The Appeal of Eloquent Events
Eloquent events are the easy button. Laravel's ORM gives you lifecycle hooks: creating, created, updating, updated, saved, deleted. You register a listener, and it fires when the event happens. No configuration, no ceremony.
Order::saved(function ($order) {
Mail::send(new OrderConfirmation($order));
});
This is clean code on the surface. The intent is clear: when an order is saved, send a confirmation. It works. And for a simple project, that's enough. The problem emerges when you have multiple listeners, or when the same model is saved for different reasons.
The Hidden Problem: Intent and Context
Here's where things break down. Eloquent's saved event fires whenever an order is saved, regardless of why it was saved. Maybe you created it. Maybe you updated the status from 'pending' to 'confirmed'. Maybe you updated the shipping address. The saved event doesn't care—it fires all the same.
Now imagine this scenario. A refund flow saves the order with a refunded status. A separate process—maybe a scheduled job, maybe a webhook from your payment processor—also updates the order. Both trigger the saved event. Two listeners run. One sends a refund email, one sends a different notification. The customer gets both messages, and they're confused about which one is real.
The root cause is that Eloquent events are tied to the database operation, not to the business action. Your code says "an order was saved," but what you really mean is "an order was created" or "a refund was issued" or "shipping details were updated." These are different actions with different side effects.
Framework Events Don't Scale
As your application grows, this problem compounds. You add a new listener. It works. You add another. Now you have five listeners on the saved event, and you can't remember what each one does. When a bug appears, you have to trace through all five to find out which one caused it. The dependencies are implicit and scattered across your codebase.
Worse, if two listeners depend on each other—one needs to run before the other—you have no way to enforce that order. Eloquent fires them in registration order, which is fragile. If someone adds a listener in the wrong place in the file, the side effects happen in the wrong order.
Domain Events: Intent as First-Class
Domain events are a different approach, borrowed from domain-driven design (DDD). Instead of relying on database operations, you emit events that represent what actually happened in your business logic.
Instead of the saved event, you'd emit an OrderCreated event or an OrderRefunded event. Each event carries the context: what happened, and why. Your listeners then subscribe to the events they care about.
class CreateOrderAction
{
public function execute(CreateOrderRequest $request)
{
$order = Order::create([...]);
event(new OrderCreated($order));
return $order;
}
}
Now your confirmation email listener only subscribes to OrderCreated, not OrderSaved. The refund email only subscribes to OrderRefunded. Each side effect is tied to the action it represents, not to the database operation.
Decoupling Your Side Effects
Domain events also decouple your domain logic from your framework. Eloquent's saved event is a Laravel concept. Domain events are not. If you ever want to move your business logic to a different framework, or use it in a console command, or test it in isolation, domain events make that possible. Framework events make it harder.
With domain events, your business logic lives in action classes or services, independent of the ORM. The framework becomes a tool you use, not something your logic is tangled up with.
A Concrete Example: The Refund Flow
Imagine a refund process. You have three things that need to happen: update the order status, deduct money from your merchant account, and send a refund email to the customer.
With Eloquent events, you'd wire listeners to the saved event. But that's sloppy. The merchant account deduction has nothing to do with the order being saved. It should happen when a refund is initiated.
class ProcessRefund
{
public function execute(Order $order, RefundDetails $details)
{
$order->status = 'refunded';
$order->save();
event(new OrderRefunded($order, $details));
}
}
Now you can listen to OrderRefunded and handle each concern separately. The merchant account deduction, the email, the accounting ledger entry—each listener handles one thing.
The Transition Path
You don't have to rip out all your Eloquent listeners today. The transition can be gradual. Start emitting domain events from your action classes. Over time, move listeners over. Delete the Eloquent listeners as you replace them.
Conclusion
Framework event hooks are convenient, but they're a shortcut that costs you clarity and maintainability as your code grows. Domain events require a bit more thought upfront, but they scale. They're explicit about what's happening and why. They decouple your business logic from your framework. When a bug appears, you know exactly where to look.
Merits
- Events represent actual business actions, not database operations
- Easy to understand which side effects trigger for which actions
- No hidden dependencies between listeners
- Framework-agnostic—your business logic isn't tied to Laravel
- Testable in isolation without framework setup
- Listeners can be ordered and coordinated deliberately
Demerits
- Requires more boilerplate than framework listeners
- Need to manually emit events; won't happen automatically
- More mental overhead during development
- Requires discipline—easy to forget to emit an event
- Debugging is slightly harder if events aren't logged
Caution
The example code and pattern names in this article are generic illustrations, not specific production code. Test your event flow thoroughly before deploying to production. Proceed at your own risk. If you're currently using Eloquent events, migrating to domain events is a refactoring task that should be done carefully, with good test coverage for your side effects.
Frequently asked questions
- What if I have multiple domains that need to react to the same action?
- How do I handle event ordering and dependencies between listeners?
- Can I use domain events with Eloquent, or do I need a different ORM?
- What happens if an event listener fails—does the whole transaction rollback?
- How do I test domain event listeners in isolation?
- Should I emit domain events before or after saving to the database?
- What's the difference between a domain event and a webhook?
- How do I handle eventual consistency when using domain events?
Tags
#laravel #domainDrivenDesign #architecture #eventSourcing #PHP #cleanArchitecture #refactoring #scalability


Responses
Sign in to leave a response.
Loading…