
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:
textOrderCreated 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:
textTraditional: Controller โ Model โ Database
CQRS architecture:
textCommand โ 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:
textspatie/laravel-event-sourcing
Installation:
bashcomposer require spatie/laravel-event-sourcing
Publish configuration:
bashphp artisan vendor:publish --tag=event-sourcing-config
Run migrations:
bashphp artisan migrate
This creates tables for:
- stored_events
- snapshots
Creating a Domain Event
Example:
bashphp artisan make:event MoneyDeposited
Event:
phpnamespace 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:
bashphp artisan make:aggregate WalletAggregate
Aggregate:
phpnamespace 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:
phpWallet::find($id)->increment('balance');
Use aggregates:
phpWalletAggregate::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:
bashphp 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:
bashphp artisan make:projector WalletProjector
Projector:
phpnamespace 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:
phpclass 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:
textCommand 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:
phpWalletAggregate::retrieve($uuid)
Instead of replaying 100,000 events:
textLoad snapshot Replay recent events only
This significantly improves performance.
Designing Event Names Properly
Events represent facts.
Good:
textPaymentProcessed UserRegistered InvoiceGenerated
Bad:
textUpdateUser 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:
phppublic function __construct( public string $userId, public string $email, public ?string $phone = null ) {}
Versioned Events
textUserRegisteredV2
Use only for breaking changes.
Event Store Database Design
Typical structure:
sqlCREATE 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:
sqlCREATE 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:
textWhat 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:
textOrderPlaced โ 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:
textDatabase Transaction: - Save business data - Save event into outbox table
Background worker:
textReads outbox Publishes to Kafka/RabbitMQ Marks processed
This prevents lost events.
Scaling Event-Sourced Systems
Partition Aggregates
Avoid hot aggregates.
Bad:
textSingle aggregate for all orders
Good:
textOne 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:
textLaravel โ 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.