Back to Blog
Lesson 58 of the Intermediate Laravel: Real-World Application Patterns course
LaravelJune 27, 20264 min read

Event Sourcing Concepts: Building Immutable Data Streams in Laravel

Master event sourcing concepts in Laravel. Learn to move beyond CRUD by using immutable event streams and projections to reconstruct domain state reliably.

LaravelEvent SourcingDomain Driven DesignArchitecturePatternsphpbackend

Previously in this course, we explored Introduction to Laravel Events and Listeners for Clean Code, where we used events to trigger side effects like sending emails or updating secondary tables. In this lesson, we shift our perspective from using events as notifications to using them as the source of truth.

Understanding Event Sourcing Principles

In a standard CRUD (Create, Read, Update, Delete) application, you store the current state of an object. If you update a task's title, the old title is gone, overwritten in the database.

Event sourcing reverses this. Instead of storing the current state, you store a sequence of immutable events—the "Event Stream"—that describe every change that has ever occurred. To find the current state of a task, you "replay" these events from the beginning.

ConceptTraditional CRUDEvent Sourcing
StorageCurrent state onlySequence of events
HistoryUsually lost or requires logsAlways available (reconstructible)
ImmutabilityMutable rowsAppend-only events
ComplexityLowHigh (requires projections)

Defining Events and Streams

An event is a statement of fact about something that happened in the past. In our project board, instead of a tasks table with a status column, we might have events like TaskCreated, TaskStatusChanged, and TaskAssigned.

An event stream is the ordered list of all events associated with a specific aggregate (e.g., one specific Task).

Implementing a Simple Event Projection

A projection is the process of taking an event stream and "projecting" it into a read model. This is the state we actually display to the user.

Let's implement a basic projection for a Task entity.

PHP
#6A9955">// 1. The Event(Fact)
class TaskStatusChanged
{
    public function __construct(
        public readonly int $taskId,
        public readonly string $newStatus,
        public readonly DateTimeImmutable $occurredAt
    ) {}
}

#6A9955">// 2. The Projection(Reconstructing State)
class TaskProjector
{
    public function project(int $taskId, array $events): array
    {
        $state = ['status' => 'pending'];

        foreach ($events as $event) {
            if ($event instanceof TaskStatusChanged) {
                $state['status'] = $event->newStatus;
            }
        }

        return $state;
    }
}

By storing these events in a table (e.g., event_store), you can replay them to reconstruct the state of any task at any point in time. This is why API Architecture Audit Logs: Implementing Immutable Event Sourcing is a natural pattern here; the audit log is your database.

Hands-on Exercise: Projecting Task Completion

In your project board, create a new TaskCompleted event.

  1. Create a class TaskCompleted that holds the taskId and completedAt.
  2. Update the TaskProjector to handle this event: if a TaskCompleted event is encountered, set the status to 'completed' and record the completed_at timestamp in your $state array.
  3. Test this by passing an array of [TaskCreated, TaskStatusChanged, TaskCompleted] into your projector and asserting that the resulting array reflects the final completed status.

Common Pitfalls

  • Event Schema Evolution: If you rename a property in an event class, old events in your database will break your projector. Always keep your event classes stable or implement versioning/upcasting logic.
  • Performance: Replaying thousands of events every time you load a page is slow. Use "Snapshots"—periodically save the state of an aggregate to a table so you only have to replay events since the last snapshot.
  • Eventual Consistency: Since the read model is built from events, it may lag slightly behind the event store. Ensure your UI handles this, perhaps by optimistic updates.

Advancing the Running Project

For our project board, we will now prepare to store these events. Create a migration for an event_store table:

PHP
Schema::create('event_store', function (Blueprint $table) {
    $table->id();
    $table->string('aggregate_type'); #6A9955">// e.g., 'Task'
    $table->string('aggregate_id');
    $table->string('event_type');
    $table->json('payload');
    $table->timestamp('created_at');
});

This table will eventually hold the history of our tasks, providing the foundation for the complex features we'll build later.

Recap

We've moved from storing "what is" to "what happened." By using immutable events as the source of truth and projections to calculate current state, we gain an audit trail by default and a flexible way to change our business logic without losing historical context.

Up next: We will explore how to manage job chains to ensure our projections are updated reliably in the background, keeping our read models in sync with our event store.

Similar Posts