
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Implementing event sourcing with Laravel Spatie package transforms your application from a state-mutation black box into a transparent, auditable system where every change is an immutable fact. While traditional CRUD overwrites data and loses history, this approach stores domain events as the single source of truth, enabling perfect audit trails and temporal queries essential for fintech and compliance-heavy environments. This guide covers the practical implementation details, trade-offs, and production patterns I use when building resilient PHP systems that must satisfy SOC 2 or ISO 27001 requirements without sacrificing developer velocity.
spatie/laravel-event-sourcing, define aggregate roots to enforce business rules, persist events to a stream, and use projectors to build read-optimized views, delivering full auditability and reliable state reconstruction for compliant applications.How do you set up event sourcing with Laravel Spatie package?
Getting started requires more than just running Composer; you need to configure storage, serialization, and queue drivers correctly to avoid production pitfalls. The package handles the heavy lifting of event storage and replay, but your configuration determines whether it scales or becomes a bottleneck. For teams managing database migrations and seeding best practices in Laravel, treating the event store schema as immutable infrastructure is critical.
Installation and core configuration
Install the package via Composer and publish the configuration and migration files. Do not skip publishing the config; the defaults are rarely suitable for production workloads.
composer require spatie/laravel-event-sourcing
php artisan vendor:publish --provider="Spatie\EventSourcing\EventSourcingServiceProvider" --tag="event-sourcing-config"
php artisan vendor:publish --provider="Spatie\EventSourcing\EventSourcingServiceProvider" --tag="event-sourcing-migrations"
php artisan migrate In config/event-sourcing.php, verify these three settings before writing any domain code:
- event_store_model: Defaults to Eloquent. For high-volume systems, consider switching to a dedicated table partitioned by
aggregate_uuidandcreated_at. - serializer: Use
Spatie\EventSourcing\EventSerializers\JsonSerializerfor readability and debugging. Only switch to PHP native serialization if you have strict performance benchmarks proving JSON is the bottleneck. - queue: Set
projectors_queueandreactors_queueto a dedicated Redis queue. Never run projectors synchronously in production unless your write volume is negligible.
Defining your first aggregate root
The aggregate root is the consistency boundary. It validates business rules and records events. Never inject services or query the database inside an aggregate; it must remain pure and deterministic.
use Spatie\EventSourcing\AggregateRoots\AggregateRoot;
class OrderAggregate extends AggregateRoot
{
private string $status = 'draft';
private float $totalAmount = 0;
public function create(string $customerId, array $items): self
{
if (empty($items)) {
throw new \DomainException('Cannot create order without items');
}
$this->recordThat(new OrderCreated(
customerId: $customerId,
items: $items,
totalAmount: array_sum(array_column($items, 'price'))
));
return $this;
}
protected function applyOrderCreated(OrderCreated $event): void
{
$this->status = 'created';
$this->totalAmount = $event->totalAmount;
}
} Note the separation: create() contains validation logic and records the event, while applyOrderCreated() mutates internal state based solely on the event payload. This dual-method pattern ensures that replaying events reconstructs state identically to the original execution.
How do projectors and reactors differ in Spatie event sourcing?
A common mistake is conflating read-model builders with side-effect handlers. Understanding this distinction prevents tangled dependencies and makes your structured logging best practices significantly easier to implement because each component has a single responsibility.
| Aspect | Projector | Reactor |
|---|---|---|
| Purpose | Build/update read-optimized projections | Trigger side effects (emails, webhooks) |
| Replay safety | Fully idempotent, safe to replay anytime | Requires guards to prevent duplicate actions |
| Database access | Writes to projection tables only | May call external APIs or message queues |
| Failure impact | Stale read model until fixed and replayed | Lost notifications or duplicate charges |
| Queue priority | High (user-facing data freshness) | Normal/Low (background processing) |
When implementing reactors that send emails or payment requests, always check whether the event is being replayed. Spatie provides $event->isReplaying() or you can track replay state via a custom middleware. Skipping this guard is the number one cause of duplicate invoices in event-sourced billing systems.
How do you handle snapshots and performance optimization?
Without snapshots, loading an aggregate with thousands of events means replaying every single one from the beginning. In production financial systems I've audited, this causes unacceptable latency after six months of activity. Snapshots serialize the aggregate's current state at regular intervals, reducing load time from O(n) events to O(1) snapshot + recent delta.
Configuring snapshot thresholds
// In config/event-sourcing.php
'snapshot' => [
'enabled' => true,
'threshold' => 100, // Take snapshot every 100 events
'model' => Spatie\EventSourcing\Snapshots\EloquentSnapshotStore::class,
], The threshold of 100 is a reasonable starting point, but tune it based on your aggregate's complexity. If applying a single event takes 2ms due to complex calculations, even 50 events may justify a snapshot. Conversely, simple aggregates with trivial apply methods can safely go to 500+.
Custom snapshot serialization
Default serialization stores all aggregate properties. For large aggregates, implement Snapshotable to control what gets persisted:
use Spatie\EventSourcing\AggregateRoots\Snapshotable;
class OrderAggregate extends AggregateRoot implements Snapshotable
{
public function getSnapshotState(): array
{
return [
'status' => $this->status,
'totalAmount' => $this->totalAmount,
// Exclude transient or derivable properties
];
}
public function restoreFromSnapshot(array $state): void
{
$this->status = $state['status'];
$this->totalAmount = $state['totalAmount'];
}
} This selective approach reduces snapshot size and avoids serializing objects that cannot be reliably unserialized across deployments. Always test snapshot restoration in CI — broken snapshots silently corrupt state.
When should you choose event sourcing over traditional CRUD?
Event sourcing is not a default choice; it is a deliberate architectural trade-off. After implementing both patterns across dozens of projects, I recommend it only when specific conditions align. Teams exploring microservices vs monolith decisions often overestimate event sourcing's benefits while underestimating its operational cost.
Choose event sourcing when:
- Regulatory compliance (SOC 2, ISO 27001, Nepal Rastra Bank directives) demands complete, tamper-proof audit trails
- Business stakeholders regularly ask "what did this record look like on date X?"
- Your domain has complex state transitions where understanding why something changed matters as much as the current value
- You need to rebuild read models retroactively when reporting requirements change
Stick with CRUD when:
- Your application is primarily a content management system or simple form processor
- The team has no prior event sourcing experience and delivery timelines are tight
- Query patterns are simple and always reflect current state only
- Storage costs are a primary concern (event stores grow monotonically)
In my experience working with Nepali fintech companies, event sourcing pays for itself within months when regulators request transaction histories. For typical e-commerce catalogs or blog platforms, it adds complexity without proportional benefit.
Making event sourcing with Laravel Spatie package production-ready
Successful adoption of event sourcing with Laravel Spatie package depends less on the library itself and more on disciplined engineering around it. Enforce aggregate purity through code review, invest in projector testing with real event fixtures, and establish snapshot monitoring before users report latency. Treat your event store as sacred infrastructure — back it up independently, monitor its growth rate, and never allow direct SQL updates to streamed events. If your team needs guidance on implementing this pattern securely or integrating it with existing compliance frameworks, reach out to discuss your architecture. Production-grade event sourcing is achievable, but only when you respect its constraints from day one.