Laravel Observers vs Model Events: Side Effects Done Right | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Laravel Observers vs. Model Events: Choosing the Right Hook for Side Effects        On this page       1. [  The Problem With Inline Model Events ](#the-problem-with-inline-model-events)
2. [  What Observers Actually Buy You ](#what-observers-actually-buy-you)
3. [  When to Stick With Inline Events ](#when-to-stick-with-inline-events)
4. [  Testing: Faking Observers Without Touching the Database ](#testing-faking-observers-without-touching-the-database)
5. [  Avoiding the Observer Bloat Trap ](#avoiding-the-observer-bloat-trap)
6. [  Takeaways ](#takeaways)

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

  #laravel   #eloquent   #observers   #testing   #architecture  

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

     11 Jul 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   The Problem With Inline Model Events  ](#the-problem-with-inline-model-events)
2. [  02   What Observers Actually Buy You  ](#what-observers-actually-buy-you)
3. [  03   When to Stick With Inline Events  ](#when-to-stick-with-inline-events)
4. [  04   Testing: Faking Observers Without Touching the Database  ](#testing-faking-observers-without-touching-the-database)
5. [  05   Avoiding the Observer Bloat Trap  ](#avoiding-the-observer-bloat-trap)
6. [  06   Takeaways  ](#takeaways)

 The Problem With Inline Model Events
------------------------------------

Eloquent ships with a rich lifecycle: `creating`, `created`, `updating`, `updated`, `saving`, `saved`, `deleting`, `deleted`, and more. The quickest way to hook into them is directly inside the model:

```php
// App\Models\Order.php
protected static function booted(): void
{
    static::created(function (Order $order) {
        SendOrderConfirmationEmail::dispatch($order);
        InventoryService::reserve($order);
        AuditLog::record('order.created', $order->id);
    });
}

```

This works — until it doesn't. Three side effects buried in `booted()` make the model hard to read, impossible to disable in tests without hacks, and a magnet for more logic over time.

What Observers Actually Buy You
-------------------------------

An observer moves each event into a named method on a dedicated class, giving you a single place to reason about lifecycle reactions:

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

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

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

```

Register it in a service provider (or via `#[ObservedBy]` in Laravel 10+):

```php
// Using the attribute — no service provider registration needed
#[ObservedBy(OrderObserver::class)]
class Order extends Model {}

```

Because the observer is resolved through the container, constructor injection works out of the box. That alone is worth the switch from closures.

When to Stick With Inline Events
--------------------------------

Observers are not always the right tool:

- **Simple, single-purpose hooks** — setting a UUID or slug on `creating` belongs in `booted()`. It is model-internal behaviour, not a cross-cutting side effect.
- **Package models you do not own** — you cannot add `#[ObservedBy]` to a vendor class; register via `Model::observe()` in a service provider instead.
- **Conditional registration** — if the hook only applies in certain contexts (e.g., a specific tenant feature flag), a closure in a service provider is clearer than an observer that checks a flag on every event.

```php
// Good: model-internal concern stays in booted()
protected static function booted(): void
{
    static::creating(function (Order $order) {
        $order->uuid ??= (string) Str::uuid();
    });
}

```

Testing: Faking Observers Without Touching the Database
-------------------------------------------------------

The biggest win observers give you is testability. Pest makes it trivial:

```php
use App\Observers\OrderObserver;
use App\Models\Order;

it('dispatches confirmation email after order creation', function () {
    Mail::fake();

    $order = Order::factory()->create();

    Mail::assertQueued(OrderConfirmationMail::class, fn ($mail) =>
        $mail->order->is($order)
    );
});

```

Need to suppress the observer entirely for a test that does not care about side effects?

```php
it('calculates totals correctly', function () {
    Order::withoutObservers(function () {
        $order = Order::factory()->create(['subtotal' => 100]);
        expect($order->total)->toBe(110); // tax applied via cast
    });
});

```

`withoutObservers` is a first-class Laravel API — no monkey-patching required.

Avoiding the Observer Bloat Trap
--------------------------------

Observers can become a second model if you are not careful. Keep them thin:

- **Dispatch jobs, do not execute work.** The observer fires synchronously inside the request cycle. Heavy logic belongs in a queued job.
- **One observer per model.** Multiple observers on the same model fire in registration order — a subtle source of bugs.
- **No cross-model writes.** An observer that saves a related model triggers *that* model's observers, creating hard-to-trace chains. Use a dedicated action or service instead.

Takeaways
---------

- Use `booted()` closures for model-internal concerns (default values, derived attributes).
- Use observers for cross-cutting side effects that benefit from DI and named methods.
- Prefer `#[ObservedBy]` in Laravel 10+ to keep registration co-located with the model.
- Keep observers as dispatchers, not executors — push real work into queued jobs.
- Use `Model::withoutObservers()` in tests to isolate the unit under test cleanly.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-observers-vs-model-events-choosing-the-right-hook-for-side-effects-1&text=Laravel+Observers+vs.+Model+Events%3A+Choosing+the+Right+Hook+for+Side+Effects) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-observers-vs-model-events-choosing-the-right-hook-for-side-effects-1) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Does using `#\[ObservedBy\]` affect performance compared to registering in a service provider?        No meaningful difference. The attribute is read once during boot and the observer is registered identically to the service provider approach. Choose based on readability — the attribute keeps registration close to the model. 

      Q02  Can I have multiple observers on the same Eloquent model?        Yes, but they fire in registration order and there is no built-in priority mechanism. Multiple observers on one model often signal that the model has too many responsibilities. Consider consolidating into one observer or extracting domain events. 

      Q03  Will observers fire when using `Model::query()-&gt;update()` or bulk inserts?        No. Mass updates and bulk inserts bypass Eloquent model hydration entirely, so no lifecycle events — and therefore no observers — are triggered. If you need hooks on bulk operations, dispatch an explicit event or job after the query. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Filament v3 Table Bulk Actions: Custom Confirmation, Progress Feedback, and Job Dispatch](https://cdn.msaied.com/412/68d290c667a619900211863678fa0b1f.png) filament laravel queues 

### Filament v3 Table Bulk Actions: Custom Confirmation, Progress Feedback, and Job Dispatch

Go beyond the default delete bulk action. Learn how to build Filament v3 bulk actions with custom confirmation...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 11 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/filament-v3-table-bulk-actions-custom-confirmation-progress-feedback-and-job-dispatch) [ ![Practical RAG in Laravel: pgvector, Embeddings, and Retrieval Pipelines](https://cdn.msaied.com/411/8d5b164ce43b1c6b9b658b7f72a0c369.png) laravel ai pgvector 

### Practical RAG in Laravel: pgvector, Embeddings, and Retrieval Pipelines

Build a production-ready retrieval-augmented generation pipeline in Laravel using pgvector, OpenAI embeddings,...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 11 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/practical-rag-in-laravel-pgvector-embeddings-and-retrieval-pipelines-1) [ ![Laravel Reverb WebSocket Scaling: Channels, Presence, and Horizontal Deployment](https://cdn.msaied.com/410/2698f47a7daff990e9807996fe0f493c.png) laravel reverb websockets 

### Laravel Reverb WebSocket Scaling: Channels, Presence, and Horizontal Deployment

Move beyond a single Reverb node. Learn how to scale Laravel Reverb horizontally with Redis pub/sub, configure...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 11 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-reverb-websocket-scaling-channels-presence-and-horizontal-deployment) 

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

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
