Master event sourcing concepts in Laravel. Learn to move beyond CRUD by using immutable event streams and projections to reconstruct domain state reliably.
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.
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.
| Concept | Traditional CRUD | Event Sourcing |
|---|---|---|
| Storage | Current state only | Sequence of events |
| History | Usually lost or requires logs | Always available (reconstructible) |
| Immutability | Mutable rows | Append-only events |
| Complexity | Low | High (requires projections) |
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).
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.
In your project board, create a new TaskCompleted event.
TaskCompleted that holds the taskId and completedAt.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.[TaskCreated, TaskStatusChanged, TaskCompleted] into your projector and asserting that the resulting array reflects the final completed status.For our project board, we will now prepare to store these events. Create a migration for an event_store table:
PHPSchema::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.
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.
Learn to build production-ready integrations by validating webhook signatures and offloading processing to queues to ensure security and system reliability.
Read moreMaster non-breaking migrations and safe rollback procedures. Learn the expand-and-contract pattern to evolve your database schema without production downtime.
Event Sourcing Concepts