Model Observers vs Events vs Listeners in Laravel: Pick the Right Tool

2026-08-277 min readTwixr Solutions

Cover image for Model Observers vs Events vs Listeners in Laravel: Pick the Right Tool

Your User model has six boot() hooks, a registered observer, and two event listeners all firing on created. Nobody on the team knows which one runs first, or why three emails are going out.

That is what happens when you pick whichever hook syntax you saw last - rather than the one that fits the job.

Laravel gives you three distinct mechanisms for reacting to things that happen in your app. They look similar on the surface. They are not the same tool.


What each one actually is

Model Observers

An observer is a plain PHP class that groups Eloquent model lifecycle callbacks: creating, created, updating, updated, deleting, deleted, and a few others.

php artisan make:observer OrderObserver --model=Order

Laravel registers it against a specific model, and every method name maps directly to an Eloquent lifecycle event. The observer only knows about that one model.

Events and Listeners

An event is a data-carrying class. It represents something that happened in your domain - not an Eloquent lifecycle moment, but a business fact.

// The event
class OrderShipped
{
    public function __construct(public Order $order) {}
}

// The listener
class SendShipmentNotification
{
    public function handle(OrderShipped $event): void
    {
        // send the email
    }
}

One event can have zero, one, or many listeners. Listeners can be queued independently. The event does not care who is listening.


The core difference - and why it matters

Eloquent model events (created, updated, etc.) are low-level hooks on persistence. They fire on every save, from anywhere in the codebase: a seeder, a migration, a background job, a test factory.

Domain events represent intentional business moments. OrderShipped fires when your fulfillment service deliberately ships an order - not whenever the shipped_at column gets a value written to it.

That distinction is the whole decision.

Side-by-side comparison of Observer vs Event+Listener flow in Laravel


When to use a Model Observer

Use an observer when:

  • The behavior is tightly coupled to the model's persistence lifecycle and has no business meaning beyond that.
  • You want to keep model-specific hooks grouped in one readable class instead of scattered across boot() methods.
  • The side effect is low-stakes and synchronous: slugifying a field, setting a default, logging a record change to an audit table.

Practical examples:

  • Auto-generating a uuid or slug on creating
  • Clearing a cache tag on updated
  • Writing to an audit log on deleted

If the observer starts calling services, dispatching jobs, or sending notifications, stop. That logic belongs in a domain event.


When to use Events and Listeners

Use events when:

  • Something business-meaningful happened and multiple parts of the system need to react independently.
  • You want each reaction to be testable in isolation.
  • Some reactions should be queued and some should run synchronously.
  • You want to add or remove reactions later without touching the place where the thing happened.

Practical examples:

  • UserRegistered - send welcome email, provision a free trial, notify Slack, create onboarding tasks. Four listeners, one event, zero coupling.
  • SubscriptionCancelled - email the user, flag the account, trigger a retention workflow.
  • OrderShipped - send a tracking notification, update inventory records, push a webhook.

The moment you find yourself writing if ($this->shouldSendEmail && $this->shouldNotifySlack) inside an observer, you have outgrown the observer.


The rule-of-thumb table

SituationBest tool
Auto-set a field before saveObserver (creating)
Keep model hooks grouped, readableObserver
Reacting to a business action from a service layerEvent + Listener
Multiple independent side effectsEvent + Listener
Side effects that must be queuedEvent + Queued Listener
Side effect involves a second model or external serviceEvent + Listener
Need to test each reaction separatelyEvent + Listener

A mistake worth naming: observers that grow teeth

The most common mess I see in Laravel codebases is an observer that starts small and reasonable, then grows:

// OrderObserver - six months in
public function updated(Order $order): void
{
    if ($order->isDirty('status') && $order->status === 'shipped') {
        Mail::to($order->user)->send(new ShipmentMail($order));
        Notification::send($order->vendor, new VendorNotification($order));
        Http::post(config('webhooks.erp'), $order->toArray());
        Cache::forget("order_{$order->id}");
    }
}

This fires on every Order::save() call across the entire codebase. A seed script, a fix-data artisan command, a test - all of them trigger this block. And you cannot queue parts of it without splitting it up anyway.

The fix: fire OrderShipped from your fulfillment service - the one place in your code where shipping intentionally happens. Let three separate queued listeners handle the email, the vendor notification, and the ERP webhook. The cache clear can stay in the observer because it genuinely is a persistence side effect.

Before/after code card: bloated observer method refactored into a dispatched domain event with separate listeners


What about model events without observers?

You can listen to Eloquent's built-in model events directly through Event::listen or $dispatchesEvents on the model:

// In Order.php
protected $dispatchesEvents = [
    'created' => OrderCreated::class,
];

This bridges the two worlds. Laravel dispatches OrderCreated automatically whenever an Order is saved for the first time. Your listeners take it from there.

This pattern is useful when you want the ergonomics of the event/listener system but need the trigger point to be the Eloquent lifecycle. Use it carefully - you are still tying business logic to persistence, so be deliberate about which events you map.


Testing each approach

Observers are tested by saving a model and asserting the side effect happened. Straightforward, but you cannot test just one reaction in isolation.

Events and listeners are separately testable:

// Test the event fires
Event::fake();
$this->fulfillmentService->ship($order);
Event::assertDispatched(OrderShipped::class);

// Test the listener in isolation
$listener = new SendShipmentNotification();
$listener->handle(new OrderShipped($order));
// assert the mail was queued

Isolation is the clearest argument for the event/listener pattern when the stakes of a reaction are high.


Quick decision flow

  1. Is this a low-level persistence side effect with no business name? Observer.
  2. Does this represent something that happened in your domain, with a name your product manager would recognize? Event.
  3. Do multiple things need to react to it, or do any reactions need to be queued? Event + Listeners.
  4. Do you need to test each reaction in isolation? Event + Listeners.
  5. Still unsure? Write the event. You can always remove a listener. You cannot easily split an observer that has grown.

Decision flowchart: Observer vs Event vs Listener - which Laravel hook to use


The short version

Observers are for model lifecycle housekeeping. Events are for business moments. The difference is not syntax - it is intent.

When you name something OrderShipped instead of hooking updated, you are documenting what happened in plain English, decoupling the trigger from every reaction, and making the codebase testable piece by piece.

That distinction will save the next developer (or you in six months) from staring at three simultaneous emails and having no idea which hook sent them.


Built something with this pattern that you'd do differently? Drop a comment - one specific tradeoff you hit.

#Laravel #PHP #SoftwareArchitecture #BackendDevelopment #CleanCode

Frequently asked questions

A model observer hooks into Eloquent's persistence lifecycle - it fires whenever a model is saved, updated, or deleted, from anywhere in the codebase. A domain event represents an intentional business moment, fired explicitly from the part of your code where that action deliberately happens. Observers are for persistence housekeeping; events are for business logic.

Enjoyed this article?

Get notified when I publish new posts on SaaS, Laravel, and remote engineering.

Keep reading

More posts

August 26, 20267 min read

Queued Jobs vs Scheduled Commands in Laravel - When Each One Actually Fits

Picking the wrong tool slows your app and burns debugging hours. Here's a practical breakdown of when to reach for a queued job versus a scheduled command in La

Read

2026-08-297 min read

Overusing `with()` and Missing the Real N+1 in Laravel

You added with() everywhere and thought you were done. But N+1 queries are still wrecking your Laravel app. Here's where they actually hide.

Read

2026-09-187 min read

Partial and Composite Indexes: The Ordering Rule That Kills Slow Queries

Get composite and partial index ordering wrong and your query planner ignores the index entirely. Here is the rule, with concrete examples.

Read