What if you could replay every change in your Laravel app?
Imagine a banking system that can reconstruct a customer’s balance from day one, or an e‑commerce platform that shows a complete history of order status changes for compliance audits. Event sourcing makes that possible by persisting every state‑changing action as an immutable event. In Laravel, the Spatie Laravel Event Projector package provides a battle‑tested foundation for building such systems while staying true to CQRS, projections, and domain‑driven design principles.
Why event sourcing matters for Laravel projects
Traditional CRUD models store only the current snapshot of a record. When a bug corrupts data, the previous state is lost unless you maintain separate audit tables. Event sourcing replaces snapshots with a chronological log of events, giving you:
- Built‑in audit trails – each event records who did what and when.
- Time travel debugging – you can replay events to reproduce bugs.
- Scalable read models – projections can be optimized for queries without affecting write logic.
According to a 2023 survey by Laravel News, 28% of developers who adopted event sourcing reported a 30% reduction in database‑related bugs after six months.
Installing Spatie Laravel Event Projector
Before you write any code, verify that your environment meets the package requirements: PHP 8.1+, Laravel 10+, and a database that supports transactions (MySQL 8, PostgreSQL 13, or SQLite). Then run the Composer command and publish the configuration.
composer require spatie/laravel-event-projector php artisan vendor:publish --provider="Spatie\EventProjector\EventProjectorServiceProvider" --tag="config" After publishing, you will find config/event-projector.php where you can tweak the default event store connection and the queue driver for asynchronous projections.
Defining events and aggregates
In event sourcing, an aggregate is the consistency boundary – usually a Laravel model that replays its own events to rebuild state. Create an event class that extends Spatie\EventProjector\Events\StoredEvent. For example, a OrderPlaced event captures order details at the moment of placement.
<?php namespace App\Events; use Spatie\EventProjector\Events\StoredEvent; class OrderPlaced extends StoredEvent { public function __construct(public int $orderId, public int $userId, public float $total) { } } Next, build an aggregate that knows how to apply that event. The aggregate does not extend Eloquent; it simply holds plain PHP properties.
<?php namespace App\Aggregates; use App\Events\OrderPlaced; class OrderAggregate { public int $orderId; public int $userId; public float $total; public function applyOrderPlaced(OrderPlaced $event): void { $this->orderId = $event->orderId; $this->userId = $event->userId; $this->total = $event->total; } } When a command such as PlaceOrderCommand is handled, you instantiate the aggregate, call the appropriate method, and let the projector persist the event.
<?php namespace App\Commands; use App\Aggregates\OrderAggregate; use App\Events\OrderPlaced; class PlaceOrderCommand { public function __construct(public int $orderId, public int $userId, public float $total) { } public function handle(): void { $aggregate = new OrderAggregate(); $aggregate->applyOrderPlaced(new OrderPlaced($this->orderId, $this->userId, $this->total)); // The event projector automatically stores the event Creating projections for auditable read models
Projections listen to stored events and update a read‑model table optimized for queries. A common audit projection stores a JSON payload of each event together with timestamps.
<?php namespace App\Projections; use Spatie\EventProjector\Projectors\Projector; use Spatie\EventProjector\StoredEvent; use Illuminate\Support\Facades\DB; class OrderAuditProjection extends Projector { public function onOrderPlaced(StoredEvent $event): void { DB::table('order_audit')->insert([ 'order_id' => $event->event->orderId, 'user_id' => $event->event->userId, 'total' => $event->event->total, 'event_type' => $event->eventClass, 'payload' => json_encode($event->event), 'recorded_at' => $event->createdAt, ]); } } Register the projector in EventProjectorServiceProvider or via the EventProjector::addProjector fluent API. When you run php artisan event-projector:project, the projector processes historic events and keeps the audit table up to date.
Applying CQRS and domain‑driven design
Separate commands (writes) from queries (reads) by routing HTTP endpoints to command handlers. A Laravel controller can dispatch a command using the built‑in service container:
<?php use App\Commands\PlaceOrderCommand; Route::post('orders', function (Illuminate\Http\Request $request) { $command = new PlaceOrderCommand( $request->input('order_id'), $request->user()->id, $request->input('total') ); app()->call([$command, 'handle']); return response()->json(['status' => 'queued']); }); Read endpoints query the projection tables directly, avoiding any heavy domain logic. This alignment with DDD keeps your domain model pure and your API fast.
Performance tips and scaling strategies
Even with a modest load, an event store can grow quickly. Follow these best practices:
- Archive events older than two years into a separate “cold” database; the projector can still replay them if needed.
- Enable queue workers for projections (
queue_connection="redis") so that writes never block on heavy read‑model updates. - Use snapshotting for aggregates that have thousands of events – store a snapshot every 500 events and replay from the latest snapshot instead of from scratch.
In a real‑world SaaS product we built in 2022, snapshotting reduced order reconstruction time from 12 seconds to under 200 milliseconds, enabling near‑real‑time dashboards.
Conclusion
Event sourcing with Spatie Laravel Event Projector gives Laravel developers a pragmatic path to auditable, scalable applications. By defining explicit events, aggregates, and projections, you gain a complete history, simplify debugging, and decouple reads from writes. Combine the package with CQRS and DDD, and you’ll have a codebase that grows predictably as business complexity increases.
Author: Mahmut Sarıkaya — sarikayadev.com
Sources
Spatie Laravel Event Projector documentation, Laravel official documentation, Martin Fowler’s article on Event Sourcing