Event Sourcing &amp; CQRS in Laravel: Aggregates &amp; Projectors | 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)    Event Sourcing and CQRS in Laravel: Aggregates, Projectors, and Eventual Consistency        On this page       1. [  Why Event Sourcing Is Not Just a Fancy Audit Log ](#why-event-sourcing-is-not-just-a-fancy-audit-log)
2. [  The Aggregate Root ](#the-aggregate-root)
3. [  Raising Events from a Command Handler ](#raising-events-from-a-command-handler)
4. [  Projectors: Building Read Models ](#projectors-building-read-models)
5. [  Reactors: Side-Effects Without Coupling ](#reactors-side-effects-without-coupling)
6. [  Snapshots for Long-Lived Aggregates ](#snapshots-for-long-lived-aggregates)
7. [  Honest Trade-offs ](#honest-trade-offs)
8. [  Key Takeaways ](#key-takeaways)

  ![Event Sourcing and CQRS in Laravel: Aggregates, Projectors, and Eventual Consistency](https://cdn.msaied.com/315/dd108db76a66655608b74653aad0a941.png)

  #laravel   #event-sourcing   #cqrs   #ddd   #architecture  

 Event Sourcing and CQRS in Laravel: Aggregates, Projectors, and Eventual Consistency 
======================================================================================

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

       Table of contents

1. [  01   Why Event Sourcing Is Not Just a Fancy Audit Log  ](#why-event-sourcing-is-not-just-a-fancy-audit-log)
2. [  02   The Aggregate Root  ](#the-aggregate-root)
3. [  03   Raising Events from a Command Handler  ](#raising-events-from-a-command-handler)
4. [  04   Projectors: Building Read Models  ](#projectors-building-read-models)
5. [  05   Reactors: Side-Effects Without Coupling  ](#reactors-side-effects-without-coupling)
6. [  06   Snapshots for Long-Lived Aggregates  ](#snapshots-for-long-lived-aggregates)
7. [  07   Honest Trade-offs  ](#honest-trade-offs)
8. [  08   Key Takeaways  ](#key-takeaways)

 Why Event Sourcing Is Not Just a Fancy Audit Log
------------------------------------------------

Most Laravel applications store *current state*. Event sourcing flips that: you store *what happened*, and derive state from the event stream. The audit log is a side-effect, not the goal. The goal is a system where every state transition is an explicit, replayable fact.

This article focuses on the mechanics — aggregates, domain events, projectors, and reactors — using [`spatie/laravel-event-sourcing`](https://github.com/spatie/laravel-event-sourcing) as the runtime, but the concepts apply regardless of library.

---

The Aggregate Root
------------------

An aggregate root is the consistency boundary. All mutations go through it; it raises events instead of writing directly to a database.

```php
use Spatie\EventSourcing\AggregateRoots\AggregateRoot;

final class OrderAggregate extends AggregateRoot
{
    private OrderStatus $status = OrderStatus::Pending;
    private int $totalCents = 0;

    public function place(array $items, int $totalCents): static
    {
        $this->recordThat(new OrderPlaced($items, $totalCents));
        return $this;
    }

    public function cancel(string $reason): static
    {
        if ($this->status !== OrderStatus::Pending) {
            throw new \DomainException('Only pending orders can be cancelled.');
        }
        $this->recordThat(new OrderCancelled($reason));
        return $this;
    }

    protected function applyOrderPlaced(OrderPlaced $event): void
    {
        $this->status = OrderStatus::Pending;
        $this->totalCents = $event->totalCents;
    }

    protected function applyOrderCancelled(OrderCancelled $event): void
    {
        $this->status = OrderStatus::Cancelled;
    }
}

```

The `apply*` methods rebuild state from the event stream. No Eloquent, no DB calls inside the aggregate.

### Raising Events from a Command Handler

```php
final class PlaceOrderHandler
{
    public function handle(PlaceOrderCommand $command): void
    {
        OrderAggregate::retrieve($command->orderId)
            ->place($command->items, $command->totalCents)
            ->persist();
    }
}

```

`persist()` writes the new events to the `stored_events` table and dispatches them to all registered projectors and reactors.

---

Projectors: Building Read Models
--------------------------------

A projector listens to events and maintains a denormalized read model — the "Q" side of CQRS.

```php
use Spatie\EventSourcing\EventHandlers\Projectors\Projector;

final class OrderSummaryProjector extends Projector
{
    public function onOrderPlaced(OrderPlaced $event, string $aggregateUuid): void
    {
        DB::table('order_summaries')->insert([
            'uuid'        => $aggregateUuid,
            'status'      => 'pending',
            'total_cents' => $event->totalCents,
            'created_at'  => now(),
        ]);
    }

    public function onOrderCancelled(OrderCancelled $event, string $aggregateUuid): void
    {
        DB::table('order_summaries')
            ->where('uuid', $aggregateUuid)
            ->update(['status' => 'cancelled']);
    }
}

```

Because projectors replay from the event log, you can **drop the `order_summaries` table and rebuild it at any time** — a superpower when requirements change.

---

Reactors: Side-Effects Without Coupling
---------------------------------------

Reactors handle side-effects (emails, webhooks, third-party calls). Unlike projectors, they are *not* replayed.

```php
use Spatie\EventSourcing\EventHandlers\Reactors\Reactor;

final class NotifyCustomerOnCancellation extends Reactor
{
    public function onOrderCancelled(OrderCancelled $event, string $aggregateUuid): void
    {
        $order = OrderSummary::where('uuid', $aggregateUuid)->firstOrFail();
        Mail::to($order->customer_email)->send(new OrderCancelledMail($order));
    }
}

```

---

Snapshots for Long-Lived Aggregates
-----------------------------------

Replaying thousands of events on every command is expensive. Snapshots cache aggregate state at a point in time:

```php
OrderAggregate::retrieve($uuid)
    ->snapshot(); // persists a snapshot; future retrieval starts from here

```

The library handles merging the snapshot with subsequent events automatically.

---

Honest Trade-offs
-----------------

Event sourcing is not free:

- **Eventual consistency** means your read models may lag behind writes. Design your UI accordingly.
- **Schema evolution** is hard. Stored events are immutable; changing an event's shape requires upcasters.
- **Debugging** requires tooling to inspect the event stream; `tinker` alone won't cut it.
- **Team onboarding** cost is real — the pattern is unfamiliar to most Laravel developers.

Reserve it for domains where auditability, temporal queries, or event-driven integrations justify the overhead. A CRUD admin panel does not.

---

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

- Aggregates enforce invariants and raise events; they never touch the database directly.
- Projectors build read models and are fully replayable from the event log.
- Reactors handle side-effects and run once — never on replay.
- Snapshots prevent performance degradation on aggregates with long event histories.
- Eventual consistency is a first-class concern, not an implementation detail to hide.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fevent-sourcing-and-cqrs-in-laravel-aggregates-projectors-and-eventual-consistency&text=Event+Sourcing+and+CQRS+in+Laravel%3A+Aggregates%2C+Projectors%2C+and+Eventual+Consistency) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fevent-sourcing-and-cqrs-in-laravel-aggregates-projectors-and-eventual-consistency) 

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

  3 questions  

     Q01  Can I use event sourcing for only part of my Laravel application?        Yes, and that is often the right call. Apply event sourcing only to the bounded contexts where auditability, replayability, or complex domain logic justify the overhead. The rest of the app can remain standard Eloquent CRUD. 

      Q02  How do I handle schema changes to stored events over time?        Use event upcasters — classes that transform an old event payload into the current shape before it reaches your aggregate or projector. Spatie's package supports upcasters natively, letting you version events without mutating historical records. 

      Q03  Is there a performance cost to replaying aggregates on every command?        Yes. For aggregates with hundreds of events, replay latency is measurable. Mitigate it with snapshots, which cache aggregate state so only events after the snapshot need replaying. 

  Continue reading

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

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

 [ ![Laravel 13: New Features, Helpers, and Practical Upgrade Notes](https://cdn.msaied.com/339/58c4fa6fe9b6d25a2dac17c621b6f4c6.png) laravel laravel-13 upgrade 

### Laravel 13: New Features, Helpers, and Practical Upgrade Notes

Laravel 13 ships with async-first defaults, a leaner bootstrapping layer, and several quality-of-life helpers....

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

 1 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-13-new-features-helpers-and-practical-upgrade-notes) [ ![Laravel 12: Structured Route Files, Slim Skeletons, and the New Application Bootstrapping](https://cdn.msaied.com/337/05b39d16d0f88a5fb94d0cf74049b88b.png) laravel laravel-12 upgrade 

### Laravel 12: Structured Route Files, Slim Skeletons, and the New Application Bootstrapping

Laravel 12 ships with a leaner skeleton, first-class route file organisation, and a revised application bootst...

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

 1 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-12-structured-route-files-slim-skeletons-and-the-new-application-bootstrapping) [ ![Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Versioning](https://cdn.msaied.com/336/89d518450335e8fcdaa5be882cf4dd3e.png) laravel api resources 

### Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Versioning

Go beyond basic API resources. Learn how to implement sparse fieldsets, conditionally load relationships, and...

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

 1 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-api-resources-sparse-fieldsets-conditional-relationships-and-versioning) 

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