Event Sourcing 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 in Laravel: Aggregates, Projectors, and Rebuilding State from Events        On this page       1. [  Why Event Sourcing Fits Laravel Better Than You Think ](#why-event-sourcing-fits-laravel-better-than-you-think)
2. [  The Event Store ](#the-event-store)
3. [  Defining an Aggregate ](#defining-an-aggregate)
4. [  Projectors as Listeners ](#projectors-as-listeners)
5. [  Replaying the Event Stream ](#replaying-the-event-stream)
6. [  Key Takeaways ](#key-takeaways)

  ![Event Sourcing in Laravel: Aggregates, Projectors, and Rebuilding State from Events](https://cdn.msaied.com/375/d5f6b9ed0a38be33a23430a1637a06d5.png)

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

 Event Sourcing in Laravel: Aggregates, Projectors, and Rebuilding State from Events 
=====================================================================================

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

       Table of contents

1. [  01   Why Event Sourcing Fits Laravel Better Than You Think  ](#why-event-sourcing-fits-laravel-better-than-you-think)
2. [  02   The Event Store  ](#the-event-store)
3. [  03   Defining an Aggregate  ](#defining-an-aggregate)
4. [  04   Projectors as Listeners  ](#projectors-as-listeners)
5. [  05   Replaying the Event Stream  ](#replaying-the-event-stream)
6. [  06   Key Takeaways  ](#key-takeaways)

 Why Event Sourcing Fits Laravel Better Than You Think
-----------------------------------------------------

Event sourcing replaces mutable row updates with an append-only log of domain events. Your current state is a *projection* of that log. Laravel's queue system, Eloquent, and service container make this surprisingly ergonomic — you don't need a dedicated framework to get started.

This article focuses on three concrete pieces: **aggregates** that emit events, **projectors** that build read models, and **replaying** the event stream safely in production.

---

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

Start with a single `stored_events` table:

```php
Schema::create('stored_events', function (Blueprint $table) {
    $table->id();
    $table->uuid('aggregate_uuid')->index();
    $table->string('aggregate_type');
    $table->string('event_class');
    $table->json('payload');
    $table->unsignedInteger('aggregate_version');
    $table->timestamps();

    $table->unique(['aggregate_uuid', 'aggregate_version']);
});

```

The unique constraint on `(aggregate_uuid, aggregate_version)` is your optimistic concurrency guard — two concurrent writes for the same version will throw, not silently overwrite.

---

Defining an Aggregate
---------------------

An aggregate reconstitutes itself by replaying its own events:

```php
final class OrderAggregate
{
    private OrderStatus $status = OrderStatus::Pending;
    private int $version = 0;
    private array $pendingEvents = [];

    public static function reconstitute(string $uuid): self
    {
        $aggregate = new self();
        $events = StoredEvent::forAggregate($uuid)->get();

        foreach ($events as $stored) {
            $event = $stored->toEvent();
            $aggregate->apply($event);
            $aggregate->version = $stored->aggregate_version;
        }

        return $aggregate;
    }

    public function place(CustomerId $customer, Money $total): void
    {
        if ($this->status !== OrderStatus::Pending) {
            throw new \DomainException('Order already placed.');
        }

        $this->recordThat(new OrderPlaced($customer, $total));
    }

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

    private function apply(object $event): void
    {
        match (true) {
            $event instanceof OrderPlaced => $this->status = OrderStatus::Active,
            $event instanceof OrderCancelled => $this->status = OrderStatus::Cancelled,
            default => null,
        };
    }

    public function persist(string $uuid): void
    {
        foreach ($this->pendingEvents as $event) {
            $this->version++;
            StoredEvent::create([
                'aggregate_uuid' => $uuid,
                'aggregate_type' => self::class,
                'event_class' => $event::class,
                'payload' => $event->toArray(),
                'aggregate_version' => $this->version,
            ]);
        }

        $this->pendingEvents = [];
    }
}

```

The aggregate never touches a read model. It only cares about its own invariants.

---

Projectors as Listeners
-----------------------

A projector listens to stored events and builds a denormalized read model:

```php
final class OrderSummaryProjector
{
    public function onOrderPlaced(OrderPlaced $event, string $aggregateUuid): void
    {
        OrderSummary::create([
            'uuid' => $aggregateUuid,
            'customer_id' => $event->customerId->value,
            'total_cents' => $event->total->cents,
            'status' => 'active',
        ]);
    }

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

```

Wire projectors through a dispatcher that maps `event_class` to handler methods:

```php
class EventDispatcher
{
    public function __construct(private array $projectors) {}

    public function dispatch(StoredEvent $stored): void
    {
        $event = $stored->toEvent();
        $method = 'on' . class_basename($event);

        foreach ($this->projectors as $projector) {
            if (method_exists($projector, $method)) {
                $projector->$method($event, $stored->aggregate_uuid);
            }
        }
    }
}

```

---

Replaying the Event Stream
--------------------------

When you add a new projector or fix a bug in an existing one, truncate the read model table and replay:

```php
class ReplayProjector extends Command
{
    protected $signature = 'events:replay {projector}';

    public function handle(EventDispatcher $dispatcher): void
    {
        $class = $this->argument('projector');
        app($class)->reset(); // truncate read model

        StoredEvent::query()
            ->orderBy('id')
            ->each(fn (StoredEvent $e) => $dispatcher->dispatch($e));

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

```

Use `each()` rather than `get()` to avoid loading the entire event log into memory. For very large streams, `cursor()` or chunked processing is preferable.

---

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

- The unique `(aggregate_uuid, aggregate_version)` index is your concurrency guard — never skip it.
- Aggregates reconstitute from their own slice of the event log; they never query read models.
- Projectors are side-effect handlers — keep them idempotent so replay is safe.
- `StoredEvent::each()` streams rows one at a time; avoid `get()` on large event tables.
- Replay is your migration strategy: add a projector, replay, swap the query target.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fevent-sourcing-in-laravel-aggregates-projectors-and-rebuilding-state-from-events&text=Event+Sourcing+in+Laravel%3A+Aggregates%2C+Projectors%2C+and+Rebuilding+State+from+Events) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fevent-sourcing-in-laravel-aggregates-projectors-and-rebuilding-state-from-events) 

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

  3 questions  

     Q01  Do I need a package like spatie/laravel-event-sourcing to implement event sourcing in Laravel?        No. A package reduces boilerplate and adds snapshot support, but the core mechanics — an append-only stored_events table, aggregates that replay events, and projectors that build read models — can be implemented with plain Eloquent and Laravel's service container. Start without a package to understand the fundamentals, then adopt one if the project warrants it. 

      Q02  How do I handle schema changes to event payloads over time?        Store events as JSON and version your upcasters. When replaying, pass each raw payload through an upcaster chain before hydrating the event class. This lets you rename fields or restructure data without touching historical records. Keep upcasters small and composable — one per breaking change per event type. 

      Q03  Is replaying the entire event log safe in a live production environment?        Yes, if your projectors are idempotent and you replay into a shadow table or truncate the read model before starting. For zero-downtime deploys, build the new projection in a separate table, swap the query target atomically once replay finishes, then drop the old table. 

  Continue reading

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

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

 [ ![Domain-Driven Design in Laravel: Value Objects, DTOs, and Actions Without Bloat](https://cdn.msaied.com/377/b5c2aef77aed51ee8694f377085af424.png) laravel ddd architecture 

### Domain-Driven Design in Laravel: Value Objects, DTOs, and Actions Without Bloat

Learn how to model domain concepts with value objects, DTOs, and single-action classes in Laravel — keeping yo...

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

 6 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/domain-driven-design-in-laravel-value-objects-dtos-and-actions-without-bloat) [ ![What's Missing from Your PHP Development Environment: Meet DDLess](https://cdn.msaied.com/379/baa8990d4c7b46d18498d69d68f9b6d2.png) DDLess PHP Debugging Laravel Tools 

### What's Missing from Your PHP Development Environment: Meet DDLess

DDLess is a PHP development workbench that brings step debugging, an in-breakpoint playground, and an interact...

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

 6 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/whats-missing-from-your-php-development-environment-meet-ddless) [ ![CQRS in Laravel Without a Framework: Commands, Handlers, and Query Objects](https://cdn.msaied.com/376/bec9da4b7a7ddeee26dac3df6f5d6c44.png) laravel cqrs architecture 

### CQRS in Laravel Without a Framework: Commands, Handlers, and Query Objects

Skip the heavy CQRS libraries. Learn how to implement commands, command handlers, and query objects in plain L...

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

 6 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/cqrs-in-laravel-without-a-framework-commands-handlers-and-query-objects) 

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