Event Sourcing in Laravel: Aggregates &amp; Projectors | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com) [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles  ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

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

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

  [ home ](https://www.msaied.com)    [ articles ](https://www.msaied.com/articles)    Event Sourcing in Laravel: Aggregates, Projectors, and Rebuilding Read Models        On this page       1. [  Why Event Sourcing Is Worth the Complexity ](#why-event-sourcing-is-worth-the-complexity)
2. [  Defining a Domain Event ](#defining-a-domain-event)
3. [  The Aggregate Root ](#the-aggregate-root)
4. [  The Event Store ](#the-event-store)
5. [  Projectors: Building Read Models ](#projectors-building-read-models)
6. [  Rebuilding Read Models Safely ](#rebuilding-read-models-safely)
7. [  Packages Worth Knowing ](#packages-worth-knowing)
8. [  Key Takeaways ](#key-takeaways)

  ![Event Sourcing in Laravel: Aggregates, Projectors, and Rebuilding Read Models](https://cdn.msaied.com/647/586a0f822614fed8091917a895ebc502.png)

  #laravel   #event-sourcing   #ddd   #eloquent   #architecture  

 Event Sourcing in Laravel: Aggregates, Projectors, and Rebuilding Read Models 
===============================================================================

     9 Sep 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said  

       Table of contents

1. [  01   Why Event Sourcing Is Worth the Complexity  ](#why-event-sourcing-is-worth-the-complexity)
2. [  02   Defining a Domain Event  ](#defining-a-domain-event)
3. [  03   The Aggregate Root  ](#the-aggregate-root)
4. [  04   The Event Store  ](#the-event-store)
5. [  05   Projectors: Building Read Models  ](#projectors-building-read-models)
6. [  06   Rebuilding Read Models Safely  ](#rebuilding-read-models-safely)
7. [  07   Packages Worth Knowing  ](#packages-worth-knowing)
8. [  08   Key Takeaways  ](#key-takeaways)

 Why Event Sourcing Is Worth the Complexity
------------------------------------------

Event sourcing replaces mutable row updates with an append-only log of domain events. Your database never forgets what happened — it only records *that* it happened. The current state is a projection of that history. For audit-heavy, financially sensitive, or highly collaborative domains, this is not over-engineering; it is the correct model.

This article focuses on the mechanics: aggregates, the event store, projectors, and the rebuild story.

---

Defining a Domain Event
-----------------------

Domain events are plain, immutable value objects. Use PHP 8.2+ readonly classes:

```php
final readonly class MoneyDeposited
{
    public function __construct(
        public string $accountId,
        public int    $amountCents,
        public string $currency,
        public \DateTimeImmutable $occurredAt,
    ) {}
}

```

No Eloquent, no infrastructure concerns — just data.

---

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

An aggregate root applies events to itself and records them for persistence. It never touches the database directly.

```php
final class BankAccount
{
    private int $balanceCents = 0;
    private array $recordedEvents = [];

    public static function open(string $id, int $initialDeposit): self
    {
        $account = new self($id);
        $account->apply(new MoneyDeposited($id, $initialDeposit, 'USD', new \DateTimeImmutable()));
        return $account;
    }

    public function deposit(int $amountCents): void
    {
        if ($amountCents apply(new MoneyDeposited($this->id, $amountCents, 'USD', new \DateTimeImmutable()));
    }

    private function apply(object $event): void
    {
        $this->recordedEvents[] = $event;
        $this->when($event);
    }

    private function when(object $event): void
    {
        match (true) {
            $event instanceof MoneyDeposited => $this->balanceCents += $event->amountCents,
            default => null,
        };
    }

    public function releaseEvents(): array
    {
        $events = $this->recordedEvents;
        $this->recordedEvents = [];
        return $events;
    }
}

```

The `when()` method is the state machine. It is also used during reconstitution — replaying stored events rebuilds the aggregate without hitting any read model.

---

The Event Store
---------------

Store events as serialised JSON rows, never update them:

```php
Schema::create('stored_events', function (Blueprint $table) {
    $table->id();
    $table->string('aggregate_id')->index();
    $table->string('aggregate_type');
    $table->string('event_class');
    $table->jsonb('payload');
    $table->unsignedBigInteger('version');
    $table->timestamp('created_at');
    $table->unique(['aggregate_id', 'version']); // optimistic concurrency
});

```

The unique constraint on `(aggregate_id, version)` is your optimistic concurrency guard — two concurrent writes for the same version will produce a database error, not silent data corruption.

---

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

A projector listens to stored events and maintains a denormalised read model:

```php
final class AccountBalanceProjector
{
    public function onMoneyDeposited(MoneyDeposited $event): void
    {
        AccountBalance::updateOrCreate(
            ['account_id' => $event->accountId],
            ['balance_cents' => \DB::raw("balance_cents + {$event->amountCents}")],
        );
    }
}

```

Projectors are side-effect machines. Keep them thin — no business logic, no conditionals beyond routing.

---

Rebuilding Read Models Safely
-----------------------------

This is the killer feature. When your read model schema changes, replay all events:

```php
final class RebuildAccountBalances extends Command
{
    protected $signature = 'projections:rebuild-balances';

    public function handle(EventStore $store, AccountBalanceProjector $projector): void
    {
        AccountBalance::truncate();

        $store->allForType(BankAccount::class)
            ->lazy()
            ->each(function (StoredEvent $stored) use ($projector) {
                $event = $stored->toEvent();
                if ($event instanceof MoneyDeposited) {
                    $projector->onMoneyDeposited($event);
                }
            });

        $this->info('Rebuild complete.');
    }
}

```

For zero-downtime rebuilds, write to a shadow table, verify, then swap with a transaction and a view rename.

---

Packages Worth Knowing
----------------------

`spatie/laravel-event-sourcing` provides a solid, production-tested foundation — aggregate roots, projectors, reactors, and a stored events table out of the box. Roll your own only if you have constraints it cannot satisfy.

---

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

- **Aggregates own business rules**; they never touch persistence directly.
- **Events are immutable facts** — readonly classes enforce this at the language level.
- **Optimistic concurrency** via a unique `(aggregate_id, version)` index prevents split-brain writes.
- **Projectors are disposable** — the event log is the truth; read models are derived and rebuildable.
- **Lazy collection replay** keeps memory flat during large rebuilds.
- Shadow-table rebuilds enable schema migrations without downtime.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fevent-sourcing-in-laravel-aggregates-projectors-and-rebuilding-read-models&text=Event+Sourcing+in+Laravel%3A+Aggregates%2C+Projectors%2C+and+Rebuilding+Read+Models) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fevent-sourcing-in-laravel-aggregates-projectors-and-rebuilding-read-models) 

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

  3 questions  

     Q01  Do I need event sourcing for every Laravel application?        No. Event sourcing adds real complexity — serialisation, replay logic, projector maintenance. It pays off in domains where audit history, temporal queries, or complex state machines are core requirements. For a standard CRUD app, it is overkill. 

      Q02  How do I handle aggregate reconstitution from the event store?        Load all stored events for a given aggregate ID ordered by version, then replay them through the aggregate's `when()` method without calling `apply()` (which would re-record them). The final in-memory state is the current aggregate state. 

      Q03  What happens if a projector fails mid-rebuild?        Track the last successfully processed event ID in a checkpoint table. On retry, resume from that checkpoint rather than replaying from the beginning. This makes rebuilds idempotent and safe to interrupt. 

  Continue reading

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

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

 [ ![Read/Write Splitting and Sticky Reads in Laravel: A Production Guide](https://cdn.msaied.com/646/4155b1eed8a491a99c3dda6f7dddd80e.png) laravel database performance 

### Read/Write Splitting and Sticky Reads in Laravel: A Production Guide

Learn how Laravel's read/write connection splitting works under the hood, when sticky reads save you from repl...

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

 9 Sep 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/readwrite-splitting-and-sticky-reads-in-laravel-a-production-guide) [ ![Chunked Iteration, Lazy Collections, and Cursor Pagination at Scale in Laravel](https://cdn.msaied.com/645/3bcda55cd0d9e4e9b4be38c9b3d11ea4.png) laravel eloquent performance 

### Chunked Iteration, Lazy Collections, and Cursor Pagination at Scale in Laravel

Processing millions of Eloquent rows without exhausting memory requires the right tool for the job. Learn when...

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

 8 Sep 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/chunked-iteration-lazy-collections-and-cursor-pagination-at-scale-in-laravel) [ ![PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Pain](https://cdn.msaied.com/643/656efe6f0c30b559bcdb27456edcc366.png) laravel postgresql eloquent 

### PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Pain

JSONB columns unlock flexible schemas inside PostgreSQL, but misused they become slow blobs. Learn how to inde...

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

 8 Sep 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/postgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-pain-2) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.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://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [Contact](https://www.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)
