Event Sourcing and CQRS in Laravel: Deep Dive into Core Components
Author: Hocine Mechouh
Published on: April 5, 2024
Event Sourcing and Command Query Responsibility Segregation (CQRS) are advanced architectural patterns that can transform how we build complex applications. Let’s dive deep into two fundamental components, with a focus on practical implementation.
1. The Heart of Event Sourcing: Domain Events
At the core of event sourcing lies Domain Events - the immutable records of every significant change in your system. Here’s a production-ready implementation:
namespace App\EventSourcing;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
abstract class DomainEvent
{
private string $eventId;
private array $metadata;
public function __construct(
private string $aggregateId,
private array $payload,
private ?Carbon $occurredAt = null
) {
$this->eventId = Str::uuid()->toString();
$this->occurredAt = $occurredAt ?? now();
$this->metadata = [
'user_id' => auth()->id(),
'ip_address' => request()->ip(),
'environment' => app()->environment(),
'correlation_id' => Str::uuid()->toString(),
];
}
abstract public function getEventName(): string;
public function getAggregateId(): string
{
return $this->aggregateId;
}
public function getPayload(): array
{
return $this->payload;
}
public function getMetadata(): array
{
return $this->metadata;
}
public function getOccurredAt(): Carbon
{
return $this->occurredAt;
}
public function getEventId(): string
{
return $this->eventId;
}
public function toArray(): array
{
return [
'event_id' => $this->eventId,
'event_type' => $this->getEventName(),
'aggregate_id' => $this->aggregateId,
'payload' => $this->payload,
'metadata' => $this->metadata,
'occurred_at' => $this->occurredAt,
];
}
}
Key Features Explained
-
Immutability The DomainEvent class ensures all properties are private with readonly getters. This guarantees that once an event is created, it cannot be modified, maintaining the integrity of your event stream.
-
Rich Metadata Every event automatically captures essential contextual information:
- User ID for comprehensive audit trails
- IP address for security monitoring
- Environment information for debugging
- Correlation ID for distributed tracing
-
Unique Identification Each event receives a UUID, enabling:
- Reliable event ordering
- Duplicate event detection
- Cross-system event correlation
2. The Event Store: Your Source of Truth
The Event Store is the backbone of any event-sourced system, responsible for reliably persisting and retrieving your domain events:
namespace App\EventSourcing;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use App\Exceptions\ConcurrencyException;
class EventStore
{
private const CACHE_TTL = 3600; // 1 hour
public function __construct(
private string $tableName = 'event_store',
private bool $useCache = true
) {}
public function append(DomainEvent $event, int $expectedVersion = null): void
{
DB::beginTransaction();
try {
// Concurrency check
if ($expectedVersion !== null) {
$currentVersion = $this->getCurrentVersion($event->getAggregateId());
if ($currentVersion !== $expectedVersion) {
throw new ConcurrencyException(
"Expected version {$expectedVersion}, but current version is {$currentVersion}"
);
}
}
// Persist the event
DB::table($this->tableName)->insert([
'event_id' => $event->getEventId(),
'aggregate_id' => $event->getAggregateId(),
'event_type' => $event->getEventName(),
'payload' => json_encode($event->getPayload()),
'metadata' => json_encode($event->getMetadata()),
'occurred_at' => $event->getOccurredAt(),
'version' => $this->getNextVersion($event->getAggregateId()),
]);
// Cache management
if ($this->useCache) {
$this->clearCache($event->getAggregateId());
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
public function getEventsForAggregate(string $aggregateId): Collection
{
$cacheKey = "events.{$aggregateId}";
if ($this->useCache && Cache::has($cacheKey)) {
return Cache::get($cacheKey);
}
$events = DB::table($this->tableName)
->where('aggregate_id', $aggregateId)
->orderBy('version')
->get()
->map(function ($event) {
return $this->deserializeEvent($event);
});
if ($this->useCache) {
Cache::put($cacheKey, $events, self::CACHE_TTL);
}
return $events;
}
}
Advanced Features Breakdown
-
Concurrency Control
- Implements optimistic concurrency control using version numbers
- Prevents data conflicts in concurrent event writes
- Throws explicit exceptions for version mismatches
-
Performance Optimization
- Built-in caching system for frequently accessed events
- Configurable cache duration
- Automatic cache invalidation when new events are appended
-
Transaction Safety
- All operations wrapped in database transactions
- Automatic rollback on failures
- Ensures data consistency
Implementation Best Practices
-
Event Design Principles
- Keep events immutable and focused
- Include all necessary context in the payload
- Use descriptive, domain-specific event names
- Version your events for future schema changes
-
Performance Considerations
- Implement caching for read-heavy scenarios
- Use batch processing for bulk operations
- Consider implementing snapshots for large event streams
- Monitor event store size and implement archiving strategies
-
Security Measures
- Encrypt sensitive payload data
- Implement proper access control
- Maintain comprehensive audit trails through metadata
- Conduct regular security reviews of stored events
Common Pitfalls and Solutions
-
Over-normalized Events
- Problem: Splitting events too finely
- Solution: Each event should represent a complete business transaction
-
Missing Context
- Problem: Insufficient information in event payload
- Solution: Include all necessary data for future processing
-
Rigid Event Schemas
- Problem: Inflexible event structures
- Solution: Design events to be extensible from the start
-
Performance Oversight
- Problem: Ignoring scaling considerations
- Solution: Implement caching and consider eventual consistency
Conclusion
This implementation provides a robust foundation for building event-sourced systems in Laravel. The combination of well-designed Domain Events and a reliable Event Store creates a powerful system for tracking and managing state changes in your application.
Remember that while event sourcing offers many benefits, it’s not always the right choice. Use it when the advantages of audit trails, complex business rules, and temporal queries outweigh the implementation complexity.
Next Steps
- Implement event versioning for schema evolution
- Add event encryption for sensitive data
- Create snapshot mechanisms for performance optimization
- Develop event replay capabilities for system recovery