Laravel Observers vs. Model Events: When to Use Each | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Laravel Observers vs. Model Events: Choosing the Right Hook for Side Effects

 Laravel Observers vs. Model Events: Choosing the Right Hook for Side Effects
=============================================================================

 Model events and observers both react to Eloquent lifecycle changes, but picking the wrong one creates tangled, untestable code. Here is a precise guide to when each belongs in a production Laravel codebase.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 16 Jun 2026 · Updated 16 Jun 2026 · 4 min read

ShareCopy linkCopied

 ![Laravel Observers vs. Model Events: Choosing the Right Hook for Side Effects](https://cdn.msaied.com/219/c16eb6ef2428da6f1d4a18025bf6c15d.png) 

  On this page +1. [The Problem With "Just Use an Observer"](#the-problem-with-quotjust-use-an-observerquot)
2. [Model Events: Inline and Intentional](#model-events-inline-and-intentional)
3. [Observers: Coordinating External Side Effects](#observers-coordinating-external-side-effects)
4. [The Pitfall: Fat Observers](#the-pitfall-fat-observers)
5. [Testing Observers Without Hitting Real Services](#testing-observers-without-hitting-real-services)
6. [Suppressing Observers When You Need To](#suppressing-observers-when-you-need-to)
7. [Decision Checklist](#decision-checklist)
8. [Takeaways](#takeaways)

 The Problem With "Just Use an Observer"
---------------------------------------

Every Laravel developer reaches for an observer the moment they need to react to a model change. Observers are convenient, but convenience without intention produces bloated observer classes that mix unrelated concerns and become impossible to test in isolation.

Model events and observers solve the same problem — reacting to Eloquent lifecycle hooks — but they have meaningfully different trade-offs. Choosing deliberately keeps your codebase maintainable.

---

Model Events: Inline and Intentional
------------------------------------

Model events are closures or method calls registered directly on the model. They are best for **simple, model-owned behaviour** that has no external dependencies.

```php
// app/Models/Invoice.php
protected static function booted(): void
{
    static::creating(function (Invoice $invoice): void {
        $invoice->uuid = (string) Str::uuid();
        $invoice->number = InvoiceNumberSequence::next();
    });
}

```

This is appropriate because:

- The logic belongs to the model's own invariants.
- There are no injected services or I/O.
- It is trivially tested by creating an `Invoice` in a feature test.

The moment you reach for `app()` or inject a service inside `booted()`, you have outgrown inline events.

---

Observers: Coordinating External Side Effects
---------------------------------------------

Observers shine when a model change must trigger **external work** — sending a notification, dispatching a job, or writing an audit log. The key discipline is keeping each observer focused on a single concern.

```php
// app/Observers/OrderObserver.php
final class OrderObserver
{
    public function __construct(
        private readonly AuditLogger $audit,
    ) {}

    public function created(Order $order): void
    {
        $this->audit->record('order.created', $order->id);
    }

    public function updated(Order $order): void
    {
        if ($order->wasChanged('status')) {
            $this->audit->record('order.status_changed', $order->id, [
                'from' => $order->getOriginal('status'),
                'to'   => $order->status,
            ]);
        }
    }
}

```

Register it in a service provider, not `AppServiceProvider`:

```php
// app/Providers/DomainServiceProvider.php
public function boot(): void
{
    Order::observe(OrderObserver::class);
}

```

Laravel resolves the observer through the container, so `AuditLogger` is injected automatically.

---

The Pitfall: Fat Observers
--------------------------

A single observer handling notifications, cache invalidation, search indexing, and audit logging is a maintenance trap. Split by concern:

```bash
OrderObserver        → audit log only
OrderSearchObserver  → Meilisearch sync
OrderCacheObserver   → cache invalidation

```

Register all three. Each remains small and independently testable.

---

Testing Observers Without Hitting Real Services
-----------------------------------------------

Observers are easy to test when dependencies are injected:

```php
// tests/Unit/Observers/OrderObserverTest.php
it('records a status change audit entry', function (): void {
    $audit  = Mockery::mock(AuditLogger::class);
    $observer = new OrderObserver($audit);

    $order = Order::factory()->make(['status' => 'shipped']);
    $order->syncOriginal(); // simulate a prior save
    $order->status = 'delivered';

    $audit->shouldReceive('record')
        ->once()
        ->with('order.status_changed', $order->id, Mockery::any());

    $observer->updated($order);
});

```

No database, no HTTP, no queue — pure unit test.

---

Suppressing Observers When You Need To
--------------------------------------

Bulk operations should skip observers to avoid thousands of side-effect calls:

```php
Order::withoutObservers(function (): void {
    Order::query()->where('migrated', false)->eachById(function (Order $order): void {
        $order->update(['migrated' => true]);
    });
});

```

This is also essential in seeders and data migrations where observers would fire redundant jobs.

---

Decision Checklist
------------------

- **Use `booted()` model events** when the logic enforces a model invariant with no I/O.
- **Use an observer** when the side effect involves external services, jobs, or notifications.
- **Split observers by concern** — one responsibility per class.
- **Inject dependencies** into observers so they remain unit-testable.
- **Use `withoutObservers()`** in bulk operations and migrations.
- **Never call `app()` inside `booted()`** — that is a sign you need an observer.

---

Takeaways
---------

- Model events are for model-owned invariants; observers are for external coordination.
- Fat observers are a code smell — split by concern, not by event type.
- Constructor injection makes observers testable without a database.
- `withoutObservers()` is a first-class tool for bulk data work, not a hack.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [eloquent](https://www.msaied.com/public/articles?search=eloquent)
- [observers](https://www.msaied.com/public/articles?search=observers)
- [model-events](https://www.msaied.com/public/articles?search=model-events)
- [clean-code](https://www.msaied.com/public/articles?search=clean-code)

 Frequently asked questions 
---------------------------

  Can I register multiple observers on the same model?Yes. Call `Model::observe()` multiple times in your service provider, once per observer class. Laravel fires each registered observer in registration order for every lifecycle event.

   Do observers fire when using Eloquent mass updates like `Model::query()-&gt;update()`?No. Mass updates bypass the Eloquent model lifecycle entirely, so neither model events nor observers are triggered. You must iterate with `each()` or `eachById()` if you need observers to fire.

   Should observers dispatch jobs or perform the work inline?Prefer dispatching a queued job from the observer. Doing heavy work inline blocks the request and makes the observer harder to test. Keep the observer thin — it should only decide \*that\* something should happen, not \*how\*.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleLaravel Telescope Alternatives: Building a Lightweight Request Inspector with Middleware](https://www.msaied.com/public/articles/laravel-telescope-alternatives-building-a-lightweight-request-inspector-with-middleware) [Next articleRead Model Projections in Laravel Without Full Event Sourcing](https://www.msaied.com/public/articles/read-model-projections-in-laravel-without-full-event-sourcing)  

   On this page
-------------

1. [The Problem With "Just Use an Observer"](#the-problem-with-quotjust-use-an-observerquot)
2. [Model Events: Inline and Intentional](#model-events-inline-and-intentional)
3. [Observers: Coordinating External Side Effects](#observers-coordinating-external-side-effects)
4. [The Pitfall: Fat Observers](#the-pitfall-fat-observers)
5. [Testing Observers Without Hitting Real Services](#testing-observers-without-hitting-real-services)
6. [Suppressing Observers When You Need To](#suppressing-observers-when-you-need-to)
7. [Decision Checklist](#decision-checklist)
8. [Takeaways](#takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
