Event Sourcing with Laravel Spatie Package

Khimananda Oli 7 min read Web Development
Event Sourcing with Laravel Spatie Package

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.

CommandCreateOrderAggregate RootOrderAggregateValidates & RecordsDomain EventsEvent StoreImmutable StreamProjectorRead Model / UIReactorSide Effects / Email
Core architecture of event sourcing with Laravel Spatie package: commands flow through aggregate roots to the event store, then fan out to projectors and reactors.

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_uuid and created_at.
  • serializer: Use Spatie\EventSourcing\EventSerializers\JsonSerializer for readability and debugging. Only switch to PHP native serialization if you have strict performance benchmarks proving JSON is the bottleneck.
  • queue: Set projectors_queue and reactors_queue to 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.

Event StreamOrderCreatedProjector✓ Builds read models✓ Idempotent & replayable✓ No external side effectsReactor✓ Sends emails / notifications✓ Calls external APIs✗ NOT safe to replay blindlyRead Databaseorders_read_tableExternal SystemEmail / Payment Gateway
Projectors build queryable read models and are safe to replay; reactors trigger irreversible side effects and require guard logic during replays.
AspectProjectorReactor
PurposeBuild/update read-optimized projectionsTrigger side effects (emails, webhooks)
Replay safetyFully idempotent, safe to replay anytimeRequires guards to prevent duplicate actions
Database accessWrites to projection tables onlyMay call external APIs or message queues
Failure impactStale read model until fixed and replayedLost notifications or duplicate charges
Queue priorityHigh (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.

Start DecisionFull audit trail legally required?YESNOComplex temporal queries needed?Use Traditional CRUDNOTeam has ES experience?Use Traditional CRUDNOUse Event SourcingUse Traditional CRUD
Decision framework for adopting event sourcing with Laravel Spatie package: proceed only when audit requirements, temporal queries, and team expertise all align.

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.

Frequently Asked Questions

It is a PHP library implementing event sourcing patterns specifically for Laravel applications.

Yes, it includes built-in snapshotting to reduce replay time for aggregates with extensive event histories.

Run composer require spatie/laravel-event-sourcing and publish the configuration and migration files via artisan vendor:publish.

While technically possible, PostgreSQL is strongly recommended because its native JSONB indexing significantly outperforms MySQL when querying large event streams or filtering by payload metadata in production environments.

Observers react to state changes after they occur, whereas event sourcing treats events as the primary source of truth. State is derived by replaying these immutable events, enabling full audit trails and temporal queries that observers cannot provide natively.

An aggregate root is the central domain object responsible for validating business rules and recording events. It ensures consistency boundaries and prevents direct state mutation outside of recorded events within the Spatie framework architecture.

Use event upcasters to transform legacy event payloads into current formats during replay. This avoids modifying historical records while ensuring projections and aggregates can process older events correctly without breaking application logic or data integrity over time.

It works well for complex domains like orders or payments but adds overhead. Evaluate if eventual consistency and replay costs align with your latency requirements before adopting event sourcing for simple CRUD-heavy sections of high-traffic applications.

Use the artisan event-sourcing:rebuild-projections command with caution. Always test rebuilds on staging first, consider using separate read models during transition, and monitor database load since replaying millions of events can cause significant temporary performance degradation.

Yes, projectors and reactors can be queued automatically. Configure queue connections in the spatie-event-sourcing config file to handle asynchronous projection updates, preventing HTTP request blocking during heavy event processing or complex read model generation tasks.

It offers AggregateRootTestCase for unit testing aggregates without database dependencies. You can assert specific events were recorded, verify state transitions, and test business rule validation in isolation using fluent assertion methods provided by the testing helpers.

Never store PII directly in event payloads. Encrypt sensitive fields before recording or reference external secure storage IDs. Remember that events are immutable; you cannot delete or modify historical records to comply with GDPR right-to-be-forgotten requests easily.

Yes, but it requires careful planning. Start with new bounded contexts rather than converting existing tables. Create initial state events from current database records and run both systems parallel during transition to validate correctness before fully committing to the event-sourced architecture.

Avoid storing derived state in events, neglecting snapshot strategies for large aggregates, and treating projections as write models. Also ensure proper transactional boundaries when recording events to prevent inconsistencies between event store and application state during failures.

Check the official Spatie documentation, GitHub discussions, and the Laravel Discord server for active community help.