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

 [ ![Laravel Telescope Alternatives: Building a Lightweight Request Inspector with Middleware](https://cdn.msaied.com/216/9b6d240010b80483f072902dafcd216c.png) laravel middleware debugging 

### Laravel Telescope Alternatives: Building a Lightweight Request Inspector with Middleware

Telescope is powerful but heavy for production. Learn how to build a focused, low-overhead request inspector u...

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

 16 Jun 2026     1 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-telescope-alternatives-building-a-lightweight-request-inspector-with-middleware) [ ![RAG Pipelines in Laravel: Chunking, Embedding, and Retrieval with pgvector](https://cdn.msaied.com/215/e037e13535aa77822f879ee829ec3f68.png) laravel ai pgvector 

### RAG Pipelines in Laravel: Chunking, Embedding, and Retrieval with pgvector

Build a production-ready Retrieval-Augmented Generation pipeline in Laravel using pgvector, OpenAI embeddings,...

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

 16 Jun 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/rag-pipelines-in-laravel-chunking-embedding-and-retrieval-with-pgvector) [ ![Laravel Pest: Architecture Tests, Mutation Testing, and Type Coverage in CI](https://cdn.msaied.com/214/0d4822fa8ee1765d0689e387dd849d92.png) laravel pest testing 

### Laravel Pest: Architecture Tests, Mutation Testing, and Type Coverage in CI

Go beyond feature tests. Learn how to enforce architectural rules, catch logic gaps with mutation testing, and...

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

 16 Jun 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-pest-architecture-tests-mutation-testing-and-type-coverage-in-ci) 

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