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 Reactors Without the Framework Tax        On this page       1. [  Why Roll Your Own (At Least Partially) ](#why-roll-your-own-at-least-partially)
2. [  The Event Store ](#the-event-store)
3. [  The Aggregate Root ](#the-aggregate-root)
4. [  Projectors: Building Read Models ](#projectors-building-read-models)
5. [  Reactors: Side Effects in Isolation ](#reactors-side-effects-in-isolation)
6. [  Replaying Projections ](#replaying-projections)
7. [  Key Takeaways ](#key-takeaways)

  ![Event Sourcing in Laravel: Aggregates, Projectors, and Reactors Without the Framework Tax](https://cdn.msaied.com/187/5b670c397535e780af70790e5d45ca19.png)

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

 Event Sourcing in Laravel: Aggregates, Projectors, and Reactors Without the Framework Tax 
===========================================================================================

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

       Table of contents

1. [  01   Why Roll Your Own (At Least Partially)  ](#why-roll-your-own-at-least-partially)
2. [  02   The Event Store  ](#the-event-store)
3. [  03   The Aggregate Root  ](#the-aggregate-root)
4. [  04   Projectors: Building Read Models  ](#projectors-building-read-models)
5. [  05   Reactors: Side Effects in Isolation  ](#reactors-side-effects-in-isolation)
6. [  06   Replaying Projections  ](#replaying-projections)
7. [  07   Key Takeaways  ](#key-takeaways)

 Why Roll Your Own (At Least Partially)
--------------------------------------

Packages like `spatie/laravel-event-sourcing` are excellent, but they carry opinions about storage, snapshots, and aggregate retrieval that can fight your domain. Understanding the primitives lets you adopt a package selectively — or build a lightweight version that fits your bounded context perfectly.

This article focuses on three concepts: **aggregates** (the write side), **projectors** (read-model builders), and **reactors** (side-effect handlers).

---

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

Every event needs a durable home before anything else happens.

```php
// database/migrations/xxxx_create_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->jsonb('payload');
    $table->unsignedBigInteger('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 produce a database-level conflict rather than silent data corruption.

---

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

```php
abstract class AggregateRoot
{
    private array $recordedEvents = [];
    protected int $aggregateVersion = 0;

    public static function retrieve(string $uuid): static
    {
        $instance = new static($uuid);
        $events = StoredEvent::forAggregate($uuid)->get();

        foreach ($events as $stored) {
            $event = unserialize($stored->payload['serialized']);
            $instance->apply($event);
            $instance->aggregateVersion = $stored->aggregate_version;
        }

        return $instance;
    }

    protected function recordThat(DomainEvent $event): void
    {
        $this->apply($event);
        $this->recordedEvents[] = $event;
    }

    public function persist(): void
    {
        foreach ($this->recordedEvents as $event) {
            $this->aggregateVersion++;
            StoredEvent::create([
                'aggregate_uuid'    => $this->uuid,
                'aggregate_type'    => static::class,
                'event_class'       => $event::class,
                'payload'           => ['serialized' => serialize($event)],
                'aggregate_version' => $this->aggregateVersion,
            ]);
            event($event); // dispatch to projectors & reactors
        }
        $this->recordedEvents = [];
    }

    abstract protected function apply(DomainEvent $event): void;
}

```

A concrete aggregate looks like this:

```php
final class OrderAggregate extends AggregateRoot
{
    private OrderStatus $status;

    public function place(CustomerId $customer, Money $total): void
    {
        // guard business rules here
        $this->recordThat(new OrderPlaced($this->uuid, $customer, $total));
    }

    public function cancel(string $reason): void
    {
        if ($this->status === OrderStatus::Shipped) {
            throw new CannotCancelShippedOrder();
        }
        $this->recordThat(new OrderCancelled($this->uuid, $reason));
    }

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

```

---

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

Projectors are plain Laravel listeners. They rebuild query-optimised tables from events.

```php
final class OrderListProjector
{
    public function onOrderPlaced(OrderPlaced $event): void
    {
        OrderReadModel::create([
            'uuid'        => $event->orderId,
            'customer_id' => $event->customerId->value(),
            'total_cents' => $event->total->cents(),
            'status'      => 'pending',
        ]);
    }

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

```

Register it in `EventServiceProvider`:

```php
protected $listen = [
    OrderPlaced::class    => [OrderListProjector::class . '@onOrderPlaced'],
    OrderCancelled::class => [OrderListProjector::class . '@onOrderCancelled'],
];

```

---

Reactors: Side Effects in Isolation
-----------------------------------

Reactors handle side effects — emails, webhooks, third-party calls — and should always run asynchronously.

```php
final class NotifyCustomerOnCancellation implements ShouldQueue
{
    public function handle(OrderCancelled $event): void
    {
        Mail::to($event->customerEmail)->send(new OrderCancelledMail($event));
    }
}

```

Because reactors are queued, a mail failure never rolls back your aggregate state. That separation is the point.

---

Replaying Projections
---------------------

The killer feature of event sourcing is replay. Drop a corrupted read model and rebuild it:

```php
artisan make:command ReplayProjection

// inside handle()
StoredEvent::query()
    ->whereIn('event_class', [OrderPlaced::class, OrderCancelled::class])
    ->chunkById(500, function ($chunk) {
        foreach ($chunk as $stored) {
            $event = unserialize($stored->payload['serialized']);
            (new OrderListProjector())->{'on' . class_basename($event)}($event);
        }
    });

```

---

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

- **The unique version constraint** is your concurrency guard — don't skip it.
- **Aggregates own business rules**; projectors own read models; reactors own side effects. Keep them separate.
- **Replay is the payoff** — design projectors to be idempotent from day one.
- **Serialize carefully**: use versioned DTOs or JSON, not raw PHP `serialize()`, in production.
- **Start with one bounded context** before applying event sourcing everywhere.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fevent-sourcing-in-laravel-aggregates-projectors-and-reactors-without-the-framework-tax&text=Event+Sourcing+in+Laravel%3A+Aggregates%2C+Projectors%2C+and+Reactors+Without+the+Framework+Tax) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fevent-sourcing-in-laravel-aggregates-projectors-and-reactors-without-the-framework-tax) 

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

  3 questions  

     Q01  Do I need a package like spatie/laravel-event-sourcing to implement event sourcing in Laravel?        No. The primitives — a stored_events table, an aggregate root base class, and standard Laravel event listeners — are enough to get started. Packages add snapshots, async projectors, and tooling that become valuable at scale, but understanding the core first prevents you from fighting the package's assumptions. 

      Q02  How do I handle aggregate version conflicts under concurrent writes?        The unique index on (aggregate_uuid, aggregate_version) causes a database-level integrity exception on concurrent writes for the same version. Catch that exception at the application boundary and retry the command after re-retrieving the aggregate, similar to optimistic locking. 

      Q03  Should projectors run synchronously or asynchronously?        Projectors that build critical read models (e.g., the list your UI queries) should run synchronously so reads are consistent immediately after a write. Reactors that trigger side effects — emails, webhooks — should always be queued to isolate failures from your aggregate's write path. 

  Continue reading

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

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

 [ ![Laravel Pennant: Feature Flags with Scope, Storage, and Custom Drivers](https://cdn.msaied.com/212/ab98aa676ce445275d736755a046b360.png) laravel feature-flags pennant 

### Laravel Pennant: Feature Flags with Scope, Storage, and Custom Drivers

Laravel Pennant ships with more power than most teams use. Learn how to scope flags to tenants, swap storage d...

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

 16 Jun 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-pennant-feature-flags-with-scope-storage-and-custom-drivers) [ ![Laravel Eloquent Global Scopes: Pitfalls, Testing, and Composing Them Safely](https://cdn.msaied.com/211/8b9b19e7ecbf690b182ffbe6bffc9530.png) laravel eloquent testing 

### Laravel Eloquent Global Scopes: Pitfalls, Testing, and Composing Them Safely

Global scopes are powerful but easy to misuse. Learn how to write, test, and safely compose Eloquent global sc...

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

 16 Jun 2026     1 min read  

  Read    

 ](https://msaied.com/articles/laravel-eloquent-global-scopes-pitfalls-testing-and-composing-them-safely) [ ![Eloquent Custom Relations: Polymorphic Pivots, HasManyThrough Tricks, and Raw Join Relations](https://cdn.msaied.com/210/b47272214946c6adcd02ddf74b7df816.png) laravel eloquent database 

### Eloquent Custom Relations: Polymorphic Pivots, HasManyThrough Tricks, and Raw Join Relations

Beyond belongsTo and hasMany lies a set of underused Eloquent relation techniques. This guide covers custom re...

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

 16 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/eloquent-custom-relations-polymorphic-pivots-hasmanythrough-tricks-and-raw-join-relations) 

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