Event Sourcing in Laravel: Aggregates &amp; Projectors | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://msaied.com) - [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [About](https://msaied.com#about)

           [  Contact](https://msaied.com#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://msaied.com)
- [ProjectsCase studies](https://msaied.com/projects)
- [ArticlesEngineering notes](https://msaied.com/articles)
- [CertificatesCredentials](https://msaied.com/certificates)
- [AboutHow I work](https://msaied.com#about)
- [ContactGet in touch](https://msaied.com#contact)

  [Start a conversation](https://msaied.com#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://msaied.com)
2. /
3. [Articles](https://msaied.com/articles)
4. /
5. Event Sourcing in Laravel: Aggregates, Projectors, and Rebuilding State from Events

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

 A practical walkthrough of event sourcing in Laravel — defining aggregates, writing projectors, and safely rebuilding read models from an immutable event stream without reaching for a heavy framework.

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

ShareCopy linkCopied

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

  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)

 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.

- [laravel](https://msaied.com/articles?search=laravel)
- [event-sourcing](https://msaied.com/articles?search=event-sourcing)
- [ddd](https://msaied.com/articles?search=ddd)
- [cqrs](https://msaied.com/articles?search=cqrs)
- [architecture](https://msaied.com/articles?search=architecture)

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

  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.

   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.

   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.

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

[Mohamed Said](https://msaied.com#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://msaied.com#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 articleLivewire v3 Islands, Lazy Components, and Deferred Loading in Practice](https://msaied.com/articles/livewire-v3-islands-lazy-components-and-deferred-loading-in-practice-2) [Next articleCQRS in Laravel Without a Framework: Commands, Handlers, and Query Objects](https://msaied.com/articles/cqrs-in-laravel-without-a-framework-commands-handlers-and-query-objects)  

   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)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://msaied.com#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://msaied.com/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://msaied.com/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://msaied.com/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://msaied.com#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://msaied.com)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/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://msaied.com/sitemap.xml)
