SYS://VISION.ACTIVE
VIEWPORT.01
LAT 28.0222° N
SIGNAL.NOMINAL
VISION Loading
Back to Blog

Event Sourcing in Laravel: Building Audit-Ready Applications

Shane Barron

Shane Barron

Laravel Developer & AI Integration Specialist

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.

Share this article
Shane Barron

Shane Barron

Strategic Technology Architect with 40 years of experience building production systems. Specializing in Laravel, AI integration, and enterprise architecture.

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