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/01M22N44A70A5MC2S599JP0MPH.webp)  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 Catch Callbacks: Reliable Async Workflows in Laravel](https://cdn.msaied.com/666/f8aaa3879dc7efd419291ffa3e0b15c1.png) laravel queues async 

### Job Batching, Chaining, and Catch Callbacks: Reliable Async Workflows in Laravel

Go beyond fire-and-forget jobs. Learn how to compose Laravel job batches, chains, and catch callbacks into rel...

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

 13 Sep 2026     4 min read  

  Read    

 ](https://msaied.com/articles/job-batching-chaining-and-catch-callbacks-reliable-async-workflows-in-laravel) [ ![Partial Indexes and Covering Indexes in PostgreSQL: A Laravel Developer's Guide](https://cdn.msaied.com/665/ced6904aad758906b6047d70ea25e267.png) postgresql laravel performance 

### Partial Indexes and Covering Indexes in PostgreSQL: A Laravel Developer's Guide

Learn how partial and covering indexes eliminate wasted index space and redundant heap fetches in Laravel apps...

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

 13 Sep 2026     4 min read  

  Read    

 ](https://msaied.com/articles/partial-indexes-and-covering-indexes-in-postgresql-a-laravel-developers-guide-1) [ ![Filament v4 at Scale: Multi-Panel Auth, Custom Panels, and Table Query Tuning](https://cdn.msaied.com/664/be327447c5231a3cb27a5df9597890dd.png) filament laravel multi-panel 

### Filament v4 at Scale: Multi-Panel Auth, Custom Panels, and Table Query Tuning

Running Filament v4 across multiple panels with distinct auth guards and thousands of rows? This guide covers...

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

 13 Sep 2026     3 min read  

  Read    

 ](https://msaied.com/articles/filament-v4-at-scale-multi-panel-auth-custom-panels-and-table-query-tuning) 

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