eventsauce/laravel-eventsauce
Laravel integration for EventSauce event sourcing. Wire message dispatchers, event repositories, and consumers into Laravel’s container with publishable config, queue-friendly dispatching, and conventions for aggregates and projections in Laravel applications.
Install the package and let Laravel auto-discovery register the provider:
composer require eventsauce/laravel-eventsauce:^0.5
If discovery is disabled, add EventSauce\LaravelEventSauce\EventSauceServiceProvider to config/app.php.
Confirm you actually got the pair you want. Since 0.5.0 the package targets EventSauce 1.x, so check both halves of the stack before writing code:
composer show eventsauce/laravel-eventsauce
composer show eventsauce/eventsauce
0.5.x + eventsauce/eventsauce:^1.0 is the supported combination. A stray 0.4.x install pulls EventSauce 0.x and none of the 1.x APIs in this guide will line up.
Publish and inspect the package config:
php artisan vendor:publish --provider="EventSauce\LaravelEventSauce\EventSauceServiceProvider"
Open config/eventsauce.php first. It is the main control surface: message repository/event store, message dispatcher, outbox, snapshots, and consumer mappings.
Read EventSauceServiceProvider before writing domain code. It shows exactly which EventSauce interfaces are bound and which config keys drive them. That is faster than guessing from the README, and it is the quickest way to see how the package translated the 1.x interfaces into bindings.
Make sure the database schema matches the configured repository/outbox implementation. If the package version you install does not ship migrations for the configured driver, create the event-store and outbox tables yourself.
First useful use case: persist and rehydrate one aggregate root. Bind an AggregateRootRepository in a service provider and resolve it from the container:
use EventSauce\EventSourcing\AggregateRootRepository;
use EventSauce\EventSourcing\MessageDispatcher;
use EventSauce\EventSourcing\MessageRepository;
$this->app->bind(OrderRepository::class, function ($app) {
return new AggregateRootRepository(
Order::class,
$app->make(MessageRepository::class),
$app->make(MessageDispatcher::class),
);
});
Check the constructor signature in the installed 1.x version — it gained optional message-decorator and snapshot-repository parameters, which is where you hang snapshots later. Then persist and retrieve:
$orders = app(OrderRepository::class);
$order = Order::place($orderId, 'SKU-1');
$orders->persist($order);
$rehydrated = $orders->retrieve($orderId);
One repository binding per aggregate. Keep domain classes free of Laravel calls and bind AggregateRootRepository instances in a dedicated EventSauceServiceProvider. Avoid resolving repositories ad hoc inside controllers.
Use sync dispatch for tests and CLI, outbox/queue for HTTP requests. Configure the dispatcher per environment. Synchronous dispatch is easier to reason about locally; production writes should go through the outbox so events are not lost if the queue is unavailable. EventSauce 1.x ships SynchronousMessageDispatcher and the asynchronous decorator — the package wires whichever your config names.
Treat the outbox as part of the write transaction. Persist the aggregate and write outbox records in the same database transaction. A separate worker, job, or artisan command publishes outbox messages to the queue. Run it under Supervisor or the scheduler.
Model consumers as idempotent message handlers. Register event-to-consumer mappings in config or wire them as Laravel listeners/jobs. Because queue delivery is at-least-once, consumers should tolerate duplicate messages:
final class SendOrderConfirmation implements MessageConsumer
{
public function __invoke(Message $message): void
{
$event = $message->payload();
if (! $event instanceof OrderPlaced) {
return;
}
SendOrderConfirmationJob::dispatch($event->orderId);
}
}
Keep side effects out of aggregates. Aggregates should record events and enforce invariants. Sending mail, calling APIs, and updating projections belong in consumers.
Use snapshots for long-lived aggregates. 1.x exposes snapshotting through a snapshot repository plus a snapshotting repository decorator; configure it once stream length becomes a performance concern, otherwise every retrieve replays the full event stream.
Reach for message upgraders before rewriting stored events. EventSauce 1.x includes upcasting support. When an event payload changes shape, register an upgrader so old streams still rehydrate, instead of hand-migrating the messages table.
Swap bindings in tests. Replace MessageRepository with EventSauce's in-memory repository and use a synchronous dispatcher. Assert on recorded messages rather than database rows where possible.
Projections are your read model. Build them as consumers that write to dedicated tables. They can be rebuilt by replaying the event stream, which is a major operational advantage if you keep consumers deterministic.
0.5.0 is a breaking upgrade, not a patch. The release moves the package from EventSauce 0.x to EventSauce 1.x. If you are on 0.4.x, your domain code is written against the old interfaces and will not compile: aggregate-root repository construction, message repository retrieval semantics, message/event serializer naming and composition, snapshot repository APIs, and dispatcher wiring all changed. Budget a real migration, do it on a branch, and get your test suite green against the in-memory repository before touching production data.
Composer will not drag you across by accident — and that cuts both ways. ^0.4 will not resolve to 0.5.0, so the upgrade is explicit. Once on 0.5, pin tightly ("eventsauce/laravel-eventsauce": "0.5.*") and check the composer.json of both packages for PHP and Laravel floors before assuming your app qualifies. Eventsauce 1.x moved the PHP requirement up, so older Laravel/PHP combinations may need an intermediate upgrade first.
The package is still pre-1.0. Semver minors are meaningful here; read the changelog for every 0.x bump rather than trusting composer update. The December 2021 staleness caveat no longer applies — 0.5.0 shows the integration is being actively maintained again — but the surface is still small and moving.
This is a thin integration, not a full event-sourcing framework. There are no Eloquent aggregate models, no Artisan generators, and no automatic projection scaffolding. You own the domain model, schema, replay strategy, and consumer lifecycle.
EventSauce major-version differences matter. Pin the EventSauce version alongside the package and re-read the provider after any bump. APIs around aggregate roots, message repositories, and serializers changed between 0.x and 1.x, and examples online are heavily skewed toward the older shape.
Serialization is your responsibility. Events should contain scalars and simple DTOs. Avoid Eloquent models, closures, resources, and framework-bound objects in event payloads.
Outbox tables need indexes and pruning. Add indexes on aggregate ID, version, and dispatch status. Plan a cleanup job for processed outbox rows.
Transactions prevent lost messages. If the aggregate is persisted but the outbox write fails outside a shared transaction, downstream consumers silently miss events.
Config caching bites. After changing config/eventsauce.php, run php artisan config:clear locally and redeploy with php artisan config:cache.
Debug at the message boundary. Log dispatched messages with a decorator around MessageDispatcher, inspect the configured event-store and outbox tables directly, and use DB::listen while tracing persistence issues.
Extension points are bindings. Customize behavior by implementing and binding MessageRepository, MessageDispatcher, message decorators, message upgraders, or outbox implementations in your own service provider. Prefer overriding bindings over editing vendor code.
How can I help you explore Laravel packages today?