Laravel Observers vs Model Events: Transaction-Safe Side-Effects | 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 Domain Side-Effects

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

 Model events and observers look similar but behave differently under bulk operations, transactions, and test isolation. Learn when each is appropriate and how to avoid silent data-consistency bugs.

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

ShareCopy linkCopied

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

  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)

 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.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [eloquent](https://www.msaied.com/public/articles?search=eloquent)
- [domain-events](https://www.msaied.com/public/articles?search=domain-events)
- [testing](https://www.msaied.com/public/articles?search=testing)

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

  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.

   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.

   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.

   ![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 articlePractical RAG in Laravel: pgvector, Embeddings, and Retrieval Pipelines](https://www.msaied.com/public/articles/practical-rag-in-laravel-pgvector-embeddings-and-retrieval-pipelines-2) [Next articleJob Batching, Chaining, and Rate-Limited Middleware in Laravel Queues](https://www.msaied.com/public/articles/job-batching-chaining-and-rate-limited-middleware-in-laravel-queues-3)  

   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)

 ###  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)
