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 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%2Fwww.msaied.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%2Fwww.msaied.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://www.msaied.com/articles) 

 [ ![DDD Value Objects and DTOs in Laravel Without the Bloat](https://cdn.msaied.com/450/41688537085b86f102fd8c219a35319f.png) laravel ddd php 

### DDD Value Objects and DTOs in Laravel Without the Bloat

Learn how to implement domain-driven value objects and data transfer objects in Laravel using PHP 8.3 readonly...

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

 21 Jul 2026     1 min read  

  Read    

 ](https://www.msaied.com/articles/ddd-value-objects-and-dtos-in-laravel-without-the-bloat) [ ![Laravel + Python FastAPI: Image OCR Demo](https://cdn.msaied.com/447/5f3d87146fbf19957973cbc88c6c0155.png) Laravel Python FastAPI 

### Laravel + Python FastAPI: Image OCR Demo

Learn how to call a Python image OCR script from Laravel using FastAPI as the bridge. This premium tutorial wa...

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

 20 Jul 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-python-fastapi-image-ocr-demo) [ ![Scaffold Laravel Packages with the `laravel package` Command in Installer v5.31.0](https://cdn.msaied.com/449/522e744d3c7bae1f214c179a83ae5b88.png) Laravel Installer Package Development CLI 

### Scaffold Laravel Packages with the `laravel package` Command in Installer v5.31.0

Laravel Installer v5.31.0 introduces a `laravel package` command to scaffold packages from the CLI, automated...

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

 20 Jul 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/scaffold-laravel-packages-with-the-laravel-package-command-in-installer-v5310) 

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