Event Sourcing in Laravel: Building Audit-Ready Applications
What is Event Sourcing?
Event sourcing is a pattern where instead of storing just the current state of your data, you store a sequence of events that led to that state. Think of it like a bank account: rather than just storing your balance, the bank stores every transaction. Your balance is derived from replaying those transactions.
This approach provides complete auditability, the ability to reconstruct state at any point in time, and powerful debugging capabilities.
When to Use Event Sourcing
Event sourcing adds complexity, so use it when the benefits outweigh the costs:
- Financial systems where audit trails are required
- Systems where understanding "how we got here" matters
- Applications requiring temporal queries (state at a specific time)
- Collaborative systems where conflicts need resolution
Implementing with Spatie's Package
The spatie/laravel-event-sourcing package provides an excellent foundation:
composer require spatie/laravel-event-sourcing
Define Events
class OrderPlaced extends ShouldBeStored
{
public function __construct(
public string $orderId,
public string $customerId,
public array $items,
public float $total
) {}
}
class OrderShipped extends ShouldBeStored
{
public function __construct(
public string $orderId,
public string $trackingNumber,
public string $carrier
) {}
}
Create an Aggregate Root
class OrderAggregate extends AggregateRoot
{
private bool $isPlaced = false;
private bool $isShipped = false;
public function place(string $customerId, array $items, float $total): self
{
if ($this->isPlaced) {
throw new OrderAlreadyPlacedException();
}
$this->recordThat(new OrderPlaced(
orderId: $this->uuid(),
customerId: $customerId,
items: $items,
total: $total
));
return $this;
}
public function ship(string $trackingNumber, string $carrier): self
{
if (!$this->isPlaced) {
throw new CannotShipUnplacedOrderException();
}
if ($this->isShipped) {
throw new OrderAlreadyShippedException();
}
$this->recordThat(new OrderShipped(
orderId: $this->uuid(),
trackingNumber: $trackingNumber,
carrier: $carrier
));
return $this;
}
protected function applyOrderPlaced(OrderPlaced $event): void
{
$this->isPlaced = true;
}
protected function applyOrderShipped(OrderShipped $event): void
{
$this->isShipped = true;
}
}
Use Projectors for Read Models
class OrderProjector extends Projector
{
public function onOrderPlaced(OrderPlaced $event): void
{
Order::create([
'uuid' => $event->orderId,
'customer_id' => $event->customerId,
'items' => $event->items,
'total' => $event->total,
'status' => 'placed',
]);
}
public function onOrderShipped(OrderShipped $event): void
{
Order::where('uuid', $event->orderId)->update([
'status' => 'shipped',
'tracking_number' => $event->trackingNumber,
'carrier' => $event->carrier,
'shipped_at' => now(),
]);
}
}
Temporal Queries
One of the most powerful features is the ability to see state at any point in time:
// Reconstruct order state as of last month
$aggregate = OrderAggregate::retrieve($orderId)
->replayUntil(now()->subMonth());
// Get all events for an order
$events = StoredEvent::where('aggregate_uuid', $orderId)
->orderBy('created_at')
->get();
Conclusion
Event sourcing is a powerful pattern that provides complete auditability and temporal capabilities. It's not for every application, but when you need these features, it's invaluable. Start with Spatie's package and build your understanding from there.
Related Articles
Need Help With Your Project?
I respond to all inquiries within 24 hours. Let's discuss how I can help build your production-ready system.
Get In Touch