Laravel Observers vs Model Events: Transaction-Safe Side-Effects | 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 Domain Side-Effects        On this page       1. [  The Problem Nobody Talks About ](#the-problem-nobody-talks-about)
2. [  Model Events: Inline and Immediate ](#model-events-inline-and-immediate)
3. [  The Bulk-Update Trap ](#the-bulk-update-trap)
4. [  Observers: Organised, but Still Synchronous ](#observers-organised-but-still-synchronous)
5. [  Transaction Safety with afterCommit ](#transaction-safety-with-codeaftercommitcode)
6. [  Test Isolation ](#test-isolation)
7. [  When to Use What ](#when-to-use-what)
8. [  Key Takeaways ](#key-takeaways)

  ![Laravel Observers vs. Model Events: Choosing the Right Hook for Domain Side-Effects](https://cdn.msaied.com/488/451564d0a43aa33809619fa299027222.png)

  #laravel   #eloquent   #domain-events   #testing  

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

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

       Table of contents

1. [  01   The Problem Nobody Talks About  ](#the-problem-nobody-talks-about)
2. [  02   Model Events: Inline and Immediate  ](#model-events-inline-and-immediate)
3. [  03   The Bulk-Update Trap  ](#the-bulk-update-trap)
4. [  04   Observers: Organised, but Still Synchronous  ](#observers-organised-but-still-synchronous)
5. [  05   Transaction Safety with afterCommit  ](#transaction-safety-with-codeaftercommitcode)
6. [  06   Test Isolation  ](#test-isolation)
7. [  07   When to Use What  ](#when-to-use-what)
8. [  08   Key Takeaways  ](#key-takeaways)

 The Problem Nobody Talks About
------------------------------

Every Laravel developer reaches for `creating`, `updated`, or `deleted` hooks early in a project. They work — until they don't. Bulk updates skip them entirely, transactions roll back after the hook already fired, and test suites become brittle because observers registered in `AppServiceProvider` bleed across test cases.

This article is about making deliberate choices, not just reaching for whichever API is closest.

---

Model Events: Inline and Immediate
----------------------------------

Model events are dispatched by Eloquent's internal `fireModelEvent()` call. You can listen to them directly on the model:

```php
protected static function booted(): void
{
    static::created(function (Order $order): void {
        Cache::tags('orders')->flush();
    });
}

```

This is fine for **low-stakes, synchronous cache busting** that belongs to the model's own concern. The closure lives next to the model, is easy to read, and is automatically unregistered when the model class is garbage-collected.

### The Bulk-Update Trap

```php
// This fires ZERO model events:
Order::where('status', 'pending')->update(['status' => 'expired']);

```

Eloquent's `Builder::update()` goes straight to the query layer. No model is hydrated, no event fires. If your observer sends emails on `updated`, those emails will never arrive for bulk operations. This is the most common silent bug I see in production codebases.

---

Observers: Organised, but Still Synchronous
-------------------------------------------

An observer groups all lifecycle hooks for a model into one class:

```php
class OrderObserver
{
    public function created(Order $order): void
    {
        dispatch(new SendOrderConfirmation($order->id));
    }

    public function deleted(Order $order): void
    {
        dispatch(new ReleaseInventory($order->id));
    }
}

```

Register it in a service provider:

```php
Order::observe(OrderObserver::class);

```

Observers are cleaner than scattered closures, but they share the same fundamental limitation: they fire **before the surrounding database transaction commits**.

### Transaction Safety with `afterCommit`

Laravel's queue system respects `$afterCommit = true` on a job, but the observer itself fires immediately. If the transaction rolls back, your dispatched job has already been pushed to the queue.

The correct pattern:

```php
class SendOrderConfirmation implements ShouldQueue
{
    public bool $afterCommit = true;
    // ...
}

```

Alternatively, wrap the dispatch explicitly:

```php
public function created(Order $order): void
{
    DB::afterCommit(fn () => dispatch(new SendOrderConfirmation($order->id)));
}

```

`DB::afterCommit()` (available since Laravel 9) queues the callback until the outermost transaction commits, or runs it immediately when there is no active transaction.

---

Test Isolation
--------------

Observers registered globally in `AppServiceProvider` fire during every test. This causes:

- Unexpected mail/queue side-effects in unit tests
- Slow test suites because observers hit external services
- False positives when a test passes only because an observer mutated state

**Disable observers per test:**

```php
it('calculates order total without side effects', function () {
    Order::withoutObservers(function () {
        $order = Order::factory()->create(['subtotal' => 100]);
        expect($order->total)->toBe(110); // tax applied by cast, not observer
    });
});

```

Or use a dedicated `WithoutModelEvents` trait in a Pest `uses()` call for an entire test file:

```php
uses(WithoutModelEvents::class);

```

---

When to Use What
----------------

| Scenario | Recommendation | |---|---| | Cache invalidation tightly coupled to model | Inline `booted()` closure | | Cross-cutting concerns (audit log, search index) | Observer class | | Side-effects that must survive a transaction | Observer + `DB::afterCommit()` | | Bulk operations | Explicit service method, no observer reliance | | Domain events with multiple listeners | Dedicated event + listeners, not model events |

---

Key Takeaways
-------------

- Bulk `update()` / `delete()` queries **never** fire model events or observer hooks.
- Always wrap queue dispatches in `DB::afterCommit()` or use `$afterCommit = true` on the job to prevent side-effects from rolled-back transactions.
- Use `Order::withoutObservers()` or `WithoutModelEvents` in tests to keep assertions focused.
- For complex domain reactions with multiple consumers, prefer a proper `Event` + `Listener` pair over an observer — it scales better and is easier to test in isolation.
- Observers are an organisational tool, not a reliability guarantee.

 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-domain-side-effects&text=Laravel+Observers+vs.+Model+Events%3A+Choosing+the+Right+Hook+for+Domain+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-domain-side-effects) 

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

  3 questions  

     Q01  Do Eloquent observers fire when using `Model::query()-&gt;update()`?        No. Any query-builder-level update or delete — including `Model::where(...)-&gt;update()` — bypasses Eloquent's event system entirely because no model instances are hydrated. You must iterate with `each()` or `cursor()` if you need events to fire, or handle the side-effect explicitly in a service method. 

      Q02  What is the difference between `DB::afterCommit()` and setting `$afterCommit = true` on a job?        `$afterCommit = true` on a job tells Laravel's queue dispatcher to hold the job in memory until the outermost transaction commits before actually writing it to the queue backend. `DB::afterCommit()` is a general-purpose callback that runs any code after commit, not just job dispatches. Both solve the same transaction-safety problem; use `$afterCommit` for jobs and `DB::afterCommit()` for arbitrary side-effects like sending notifications directly. 

      Q03  Should I register observers in `AppServiceProvider` or in a dedicated provider?        For small applications, `AppServiceProvider` is fine. In a modular monolith or bounded-context architecture, register observers inside the domain's own service provider so each module is self-contained and the registration is co-located with the domain code it belongs to. 

  Continue reading

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

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

 [ ![Job Batching, Chaining, and Rate-Limited Middleware in Laravel Queues](https://cdn.msaied.com/489/89d47dc6b618d5435f9d7f333b75e922.png) laravel queues jobs 

### Job Batching, Chaining, and Rate-Limited Middleware in Laravel Queues

Go beyond basic dispatch: learn how to compose Laravel job batches with callbacks, chain dependent jobs safely...

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

 30 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/job-batching-chaining-and-rate-limited-middleware-in-laravel-queues-3) [ ![Practical RAG in Laravel: pgvector, Embeddings, and Retrieval Pipelines](https://cdn.msaied.com/487/2db17c4f7927d4a229a4786c03e7b67b.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 

 30 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/practical-rag-in-laravel-pgvector-embeddings-and-retrieval-pipelines-2) [ ![Laravel Octane + FrankenPHP: Persistent Services, Boot-Once Singletons, and Safe State](https://cdn.msaied.com/486/7a50572cb8b282ae36063a9213f55ed3.png) laravel octane frankenphp 

### Laravel Octane + FrankenPHP: Persistent Services, Boot-Once Singletons, and Safe State

Running Laravel under FrankenPHP with Octane means your service container lives across requests. Learn which s...

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

 30 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-octane-frankenphp-persistent-services-boot-once-singletons-and-safe-state) 

   [  ![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)
