HomePortfolioNewsGalleryContact
LaravelEvent SourcingCQRS

Event Sourcing and CQRS in Laravel: Building Scalable Enterprise Systems

By Aditya Nursyahbani5 min read72
Share:
Event Sourcing and CQRS in Laravel: Building Scalable Enterprise Systems

As Laravel applications grow in complexity, traditional CRUD architectures often become difficult to maintain. Systems with heavy auditing requirements, complex workflows, real-time analytics, or high write throughput can quickly outgrow standard relational approaches.

This is where Event Sourcing and CQRS (Command Query Responsibility Segregation) become powerful architectural patterns.

Large-scale systems such as banking platforms, logistics systems, healthcare applications, and SaaS products increasingly adopt these patterns to improve:

  • Scalability
  • Auditability
  • Reliability
  • Temporal debugging
  • Distributed processing
  • Real-time projections
  • Domain modeling

In this guide, we will build a deep technical understanding of Event Sourcing and CQRS using Laravel.


Understanding Traditional CRUD Limitations

Typical Laravel applications use mutable state.

Example:

php
$order->status = 'paid'; $order->save();

The previous state disappears.

Problems:

  • Difficult auditing
  • Limited debugging
  • No historical replay
  • Hard to rebuild projections
  • Complex distributed systems
  • Difficult event integrations

Traditional CRUD stores current state.

Event Sourcing stores every state transition.


What Is Event Sourcing?

Instead of storing the latest state, Event Sourcing stores immutable events.

Example:

text
OrderCreated ItemAddedToCart PaymentProcessed InvoiceGenerated OrderShipped

The current state becomes a projection generated from events.

The database acts as an append-only event log.


What Is CQRS?

CQRS separates:

  • Commands โ†’ write operations
  • Queries โ†’ read operations

Instead of one model doing everything:

text
Traditional: Controller โ†’ Model โ†’ Database

CQRS architecture:

text
Command โ†’ Aggregate โ†’ Events โ†’ Projection Query โ†’ Read Model

Benefits:

  • Optimized reads
  • Independent scaling
  • Better domain modeling
  • Reduced coupling
  • Flexible projections

Core Components in Event Sourcing

A production-grade Laravel event sourcing system usually contains:

  • Commands
  • Aggregates
  • Domain Events
  • Event Store
  • Projectors
  • Reactors
  • Read Models
  • Snapshots

Laravel Event Sourcing Packages

The most mature package:

text
spatie/laravel-event-sourcing

Installation:

bash
composer require spatie/laravel-event-sourcing

Publish configuration:

bash
php artisan vendor:publish --tag=event-sourcing-config

Run migrations:

bash
php artisan migrate

This creates tables for:

  • stored_events
  • snapshots

Creating a Domain Event

Example:

bash
php artisan make:event MoneyDeposited

Event:

php
namespace App\Domain\Wallet\Events; use Spatie\EventSourcing\StoredEvents\ShouldBeStored; class MoneyDeposited extends ShouldBeStored { public function __construct( public string $walletUuid, public int $amount ) {} }

Events should be immutable.

Never update historical events.


Building Aggregates

Aggregates enforce domain consistency.

Example:

bash
php artisan make:aggregate WalletAggregate

Aggregate:

php
namespace App\Domain\Wallet\Aggregates; use App\Domain\Wallet\Events\MoneyDeposited; use App\Domain\Wallet\Events\MoneyWithdrawn; use Spatie\EventSourcing\AggregateRoots\AggregateRoot; class WalletAggregate extends AggregateRoot { private int $balance = 0; public function deposit(int $amount): self { $this->recordThat(new MoneyDeposited( $this->uuid(), $amount )); return $this; } public function withdraw(int $amount): self { if ($this->balance < $amount) { throw new Exception('Insufficient balance'); } $this->recordThat(new MoneyWithdrawn( $this->uuid(), $amount )); return $this; } protected function applyMoneyDeposited(MoneyDeposited $event): void { $this->balance += $event->amount; } protected function applyMoneyWithdrawn(MoneyWithdrawn $event): void { $this->balance -= $event->amount; } }

The aggregate reconstructs state from historical events.


Dispatching Commands

Instead of directly updating models:

php
Wallet::find($id)->increment('balance');

Use aggregates:

php
WalletAggregate::retrieve($walletUuid) ->deposit(50000) ->persist();

The persist() method stores events atomically.


Understanding Event Replay

One of the biggest advantages of Event Sourcing:

You can rebuild application state anytime.

Example:

bash
php artisan event-sourcing:replay

Use cases:

  • Rebuild projections
  • Fix corrupted read models
  • Generate new analytics
  • Migrate business rules
  • Debug historical state

Traditional CRUD systems cannot do this reliably.


Building Projections

Projections create optimized read models.

Example:

bash
php artisan make:projector WalletProjector

Projector:

php
namespace App\Projectors; use App\Models\Wallet; use App\Domain\Wallet\Events\MoneyDeposited; use App\Domain\Wallet\Events\MoneyWithdrawn; use Spatie\EventSourcing\EventHandlers\Projectors\Projector; class WalletProjector extends Projector { public function onMoneyDeposited(MoneyDeposited $event) { Wallet::updateOrCreate( ['uuid' => $event->walletUuid], [ 'balance' => DB::raw("balance + {$event->amount}") ] ); } public function onMoneyWithdrawn(MoneyWithdrawn $event) { Wallet::where('uuid', $event->walletUuid) ->decrement('balance', $event->amount); } }

Read models are optimized for queries.


Read Models vs Write Models

CQRS allows independent optimization.

Write side:

  • Strict consistency
  • Domain rules
  • Validation
  • Transactions

Read side:

  • Fast queries
  • Denormalized tables
  • Aggregated data
  • Search indexes
  • Analytics

This separation dramatically improves scalability.


Async Projectors and Queue Processing

Projectors can run asynchronously.

Example:

php
class WalletProjector extends Projector implements ShouldQueue { }

Benefits:

  • Faster writes
  • Better throughput
  • Horizontal scalability
  • Distributed event processing

Tradeoff:

Read models become eventually consistent.


Eventual Consistency

In CQRS systems:

Writes may complete before projections update.

Example:

text
Command accepted โ†“ Event stored โ†“ Projection updated asynchronously

Users may briefly see stale data.

This is normal.

Enterprise systems often prioritize availability and scalability over immediate consistency.


Snapshotting Aggregates

Rebuilding aggregates from thousands of events can become slow.

Snapshots solve this.

Example:

php
WalletAggregate::retrieve($uuid)

Instead of replaying 100,000 events:

text
Load snapshot Replay recent events only

This significantly improves performance.


Designing Event Names Properly

Events represent facts.

Good:

text
PaymentProcessed UserRegistered InvoiceGenerated

Bad:

text
UpdateUser SaveOrder ModifyCart

Events should describe something that already happened.


Event Versioning Strategies

Events are immutable.

You cannot safely modify historical payloads.

Strategies:

Additive Changes

Preferred approach:

php
public function __construct( public string $userId, public string $email, public ?string $phone = null ) {}

Versioned Events

text
UserRegisteredV2

Use only for breaking changes.


Event Store Database Design

Typical structure:

sql
CREATE TABLE stored_events ( id BIGINT PRIMARY KEY, aggregate_uuid VARCHAR(36), event_class VARCHAR(255), event_properties JSON, meta_data JSON, created_at TIMESTAMP );

Indexes are critical.

Recommended indexes:

sql
CREATE INDEX stored_events_aggregate_uuid_index ON stored_events(aggregate_uuid); CREATE INDEX stored_events_event_class_index ON stored_events(event_class);

Building Temporal Queries

Event sourcing enables time travel.

Example:

text
What was the wallet balance on January 10?

You can replay events until a timestamp.

Traditional CRUD systems often cannot answer this accurately.


Multi-Service Integration

Events naturally support microservices.

Example flow:

text
OrderPlaced โ†“ Inventory Service โ†“ Billing Service โ†“ Shipping Service โ†“ Notification Service

This enables loosely coupled architectures.


Outbox Pattern in Laravel

One major challenge:

Ensuring database commits and message publishing remain consistent.

The Outbox Pattern solves this.

Example:

text
Database Transaction: - Save business data - Save event into outbox table

Background worker:

text
Reads outbox Publishes to Kafka/RabbitMQ Marks processed

This prevents lost events.


Scaling Event-Sourced Systems

Partition Aggregates

Avoid hot aggregates.

Bad:

text
Single aggregate for all orders

Good:

text
One aggregate per order

Use Append-Only Storage

Event stores perform well because writes are sequential.

Append-only architectures reduce lock contention.


Optimize Projection Pipelines

High-throughput systems may process:

  • Millions of events
  • Parallel consumers
  • Distributed projectors
  • Batched projections

Projection optimization becomes critical.


Monitoring Event Pipelines

Important metrics:

  • Projection lag
  • Failed projections
  • Queue depth
  • Event throughput
  • Replay duration
  • Snapshot generation time

Without monitoring, debugging distributed systems becomes extremely difficult.


When NOT to Use Event Sourcing

Event sourcing introduces complexity.

Avoid it for:

  • Simple CRUD apps
  • Small internal dashboards
  • Low-scale systems
  • MVP products
  • Basic CMS applications

Traditional Laravel architecture is often sufficient.

Use Event Sourcing only when its advantages justify the operational complexity.


Real-World Use Cases

Excellent domains for Event Sourcing:

  • Banking systems
  • Trading platforms
  • Healthcare records
  • Audit-heavy enterprise systems
  • Logistics tracking
  • IoT event processing
  • Real-time analytics
  • SaaS billing systems
  • Multi-tenant enterprise applications

Combining Laravel Horizon and Event Sourcing

Laravel Horizon works extremely well for:

  • Async projectors
  • Reactor queues
  • Replay jobs
  • Event consumers
  • Dead letter handling

Recommended production setup:

text
Laravel โ†“ Redis Queue โ†“ Horizon โ†“ Projectors / Reactors

This architecture scales efficiently under heavy workloads.


Final Thoughts

Event Sourcing and CQRS fundamentally change how applications are designed.

Instead of thinking about current state, you begin thinking in:

  • Facts
  • Events
  • Timelines
  • Projections
  • Distributed consistency

Laravel provides an excellent ecosystem for implementing these enterprise patterns while maintaining developer productivity.

However, Event Sourcing is not a silver bullet.

It increases:

  • Architectural complexity
  • Operational requirements
  • Learning curve
  • Infrastructure needs

But for systems requiring:

  • Full audit history
  • Massive scalability
  • Complex domain logic
  • Event-driven integrations
  • Temporal debugging
  • Distributed processing

It can become one of the most powerful architectural approaches available.