The foundation for event-driven domains

Build systems that remember every change. patchlevel gives PHP teams production-ready event sourcing: append-only history, reliable async projections, GDPR-aware operations, and first-class Symfony and Laravel integrations.

400k+
Packagist installs
MIT
Open source, forever
PHP 8.2+
Modern, typed, fast
5ms
to replay 1,000 events
From event to read model

How it works

Four moving parts. Events are the truth, the store is the log, the subscription engine fans events out, projections build read models. That's the whole picture.

Step

Define an event

Pure PHP classes. No base class, no magic, just immutable data with one attribute that gives it a name in your store.

HotelCreated.php
#[Event('hotel.created')]
final readonly class HotelCreated
{
    public function __construct(
        public Uuid $hotelId,
        public string $name,
    ) {}
}
Step

Build business logic

Aggregates capture changes as events and define business rules. Call recordThat and patchlevel handles persistence, replay, and ordering for you.

Hotel.php
#[Aggregate('hotel')]
final class Hotel extends BasicAggregateRoot
{
    #[Id]
    private Uuid $hotelId;

    public static function create(Uuid $hotelId, string $name): self
    {
        $self = new self();
        $self->recordThat(new HotelCreated($hotelId, $name));

        return $self;
    }

    #[Apply]
    public function onHotelCreated(HotelCreated $event): void
    {
        $this->hotelId = $event->hotelId;
    }
}
Step

Persist & load

Save the aggregate. Load it back later and its full event history is replayed automatically to reconstruct state.

example.php
$hotelId = Uuid::generate();
$hotel = Hotel::create($hotelId, 'Patchlevel HQ');

// save the aggregate
$repository = $repositoryManager->get(Hotel::class);
$repository->save($hotel);

// load it back - events are replayed automatically
$hotel = $repository->load($hotelId);
Step

React to events

Subscribers react to events asynchronously to build read models, fire side effects, or feed external systems. Versioned, gap-detected, retried on failure.

HotelProjection.php
#[Projector('hotels')]
final class HotelProjection
{
    #[Subscribe(HotelCreated::class)]
    public function onHotelCreated(HotelCreated $event): void
    {
        $this->db->insert('hotels', [
            'id' => $event->hotelId->toString(),
            'name' => $event->name,
        ]);
    }
}
Projectors & processors

The Subscription Engine

The Subscription Engine powers reliable event processing for projections, processors, and asynchronous workflows. It manages subscriber execution, replay, and recovery, ensuring events are processed consistently and at scale.

  • Subscriber lifecycle management

    Boot, replay, catch up, pause, reactivate, or retire subscribers with a fully managed lifecycle.

  • Reliable event processing

    Gap detection, retry strategies, and failure handling ensure events are processed consistently, even under load.

  • Replay & rebuild

    Rebuild projections, rerun processors, or migrate data directly from the event stream.

  • Versioned subscribers

    Deploy new subscriber versions alongside existing ones and migrate safely using blue-green deployment strategies.

  • High-performance batching

    Process events in batches for significantly faster catch-ups and rebuilds.

  • Flexible execution modes

    Run subscriptions from the beginning, from now, or as a one-time execution.

Process million-event projections without losing your weekend.

NewGuestProcessor.php
#[Processor('new-guest-mail')]
final class NewGuestProcessor
{
    #[Subscribe(GuestCheckedIn::class)]
    public function onGuestCheckedIn(GuestCheckedIn $event): void
    {
        $this->mailer->send(
            'new-guest.html',
            [
                'name' => $event->guestName,
                // more
            ],
        );
    }

    #[OnFailed]
    public function onFailed(Message $message, Throwable $e): void
    {
        $this->deadLetterQueue->push($message, $e);
    }
}
$ bin/console event-sourcing:subscription:status
id position status
new_guest_mail 1337 active
hotel_list_v3 926 detached
hotel_list_v4 1337 active
guest_list_v1 1244 paused
new_year_24_market_mail 193 detached
Sensitive data

GDPR without rewriting history

Mark personal fields with an attribute. The hydrator encrypts them and keeps the keys in a separate store. Delete a subject's key and their data is unreadable forever. The event store stays untouched, the history intact, the audit trail preserved.

CustomerRegistered.php
#[Event('customer.registered')]
final readonly class CustomerRegistered
{
    public function __construct(
        #[DataSubjectId]
        public Uuid $id,
        #[PersonalData(fallback: 'anon@patchlevel.dev')]
        public string $email,
        #[PersonalData]
        public string|null $name,
    ) {}
}

// later - right-to-be-forgotten
$cipherKeyStore->remove($customerId);

Fast by default

Powered by patchlevel/hydrator. The encryption layer adds no measurable hot-path overhead.

Attribute-driven

Add attributes, the hydrator handles the rest: encrypt on write, decrypt on read. Keys saved in separate store.

Audit-compliant

Right-to-be-forgotten without breaking your source of truth. The event log stays, just unreadable for shredded subjects.
Consistency boundaries

Beyond a single aggregate

Real-world business rules often span multiple aggregates. Two complementary patterns let you model shared consistency boundaries. One is production-ready today, the other is not released yet.

Micro Aggregates

Split a complex aggregate into focused roots that share a single event stream via the #[Stream] attribute. Each root stays small and intentional, while related entities remain transactionally consistent.

#[Aggregate('booking')]
final class Booking extends BasicAggregateRoot
{
    // ...
}

#[Aggregate('booking_payment')]
#[Stream(Booking::class)]
final class BookingPayment extends BasicAggregateRoot
{
    // ...
}
Coming Soon

Dynamic Consistency Boundary

Make consistent cross-stream decisions without loading any aggregate at all. Tag events with #[EventTag] and build minimal, purpose-built state from just the events that matter, with an optimistic append condition to keep it race-free.

#[Event('hotel.guest_checked_in')]
final readonly class GuestIsCheckedIn
{
    public function __construct(
        #[EventTag(prefix: 'hotel')]
        public Uuid $hotelId,
        #[EventTag(prefix: 'guest')]
        public string $guestName,
    ) {}
}
Production features

Tools for growing systems

Everything you need to manage complex event streams at scale, in one cohesive library.

Upcasting & Store Migration

Events evolve as your business does. Transform old payloads on the fly with upcasters, or permanently rewrite history into a new store when you're ready to retire legacy formats.

Modern Hydrator

Typed, fast object hydration designed specifically for events. Powers GDPR shredding and message decoration with zero hot-path overhead.

Snapshots

Automatic, cache-backed snapshots for long-lived aggregates. Replays stay fast even as event history grows. Opt in per aggregate when you actually need it.

DBAL Storage

The Doctrine DBAL store ships out of the box, so the event store runs on the database you already operate. Nothing extra to deploy, back up, or monitor.
  • PostgreSQL
  • MySQL
  • MariaDB
  • SQLite
Need a different store? Clean interfaces make a custom store for Kurrent, or anything else, straightforward.

Built for Testing

First-class "Given, When, Then" with in-memory stores. Lightning-fast unit tests for business logic.

$this->given(new HotelCreated($id, 'Grand Budapest'))
    ->when(new CheckInGuest($id, 'Zero'))
    ->then(new GuestCheckedIn($id, 'Zero'));

Split Stream

Long-lived aggregates that stay fast forever. Archive past lifecycle phases such as billing cycles, seasons, or contract years, then load only the active slice while keeping full history.

Static Analysis First

Deep PHPStan and Psalm integration. Type-safe repositories, custom rules, and analysis that catches mistakes before tests run.

Command & Query Bus

Built-in command and query handling with attribute-driven handlers in your aggregates or projections. Fully compatible with Symfony Messenger.

Fits your Stack

Deep Framework Integration

Stop wasting time on boilerplate. Our integrations handle the plumbing so you can focus on your business logic.

Symfony

Symfony Bundle

patchlevel/event-sourcing-bundle

Drop the bundle in and every repository, store, and subscriber wires itself into the container. Messenger, Doctrine, and the profiler are all integrated.

  • Autowiring for repositories, subscribers, listeners, upcasters & decorators
  • Symfony Messenger as command, query & event bus
  • Console commands for replay, schema & subscription lifecycle
  • Doctrine migrations for the event store schema
  • Snapshot store via Symfony Cache (any PSR-6 pool)
  • Flex recipe with zero config on install
Terminal
$ composer require patchlevel/event-sourcing-bundle
Laravel

Laravel Package

patchlevel/laravel-event-sourcing

A native Laravel feel. The service provider auto-registers everything, Artisan picks up the commands, and facades give you easy access.

  • Service provider with auto-discovery & binding
  • Artisan commands for replay, schema & subscription lifecycle
  • Facades for easy access to event sourcing services
  • Publishable config & migrations
  • Middleware for HTTP-aware features
  • Plays nicely with existing Eloquent-driven code
Terminal
$ composer require patchlevel/laravel-event-sourcing
Before you start

Frequently asked questions

The short version of what developers ask us before they start event sourcing with patchlevel.

Is the library free to use?

Yes, and that is our promise: it will never cost anything. The event sourcing library and all integrations are open source under the MIT license, with no paid tiers and no feature gates, not today and not in the future. Every feature you see here, from snapshots to subscriptions, is and stays free. If you want hands-on help, the patchlevel team also offers professional consulting and training.

What about CQRS?

Event sourcing and CQRS are a natural fit, and the library supports both: aggregates handle your writes, while subscriptions build dedicated read models (projections) optimized for your queries. You can wire in any command bus such as Symfony Messenger, or none at all. CQRS is supported, not forced.

Do I need a special event store database?

No. The event store runs on the relational database you already operate: PostgreSQL, MariaDB, MySQL, or SQLite, powered by the battle-tested Doctrine DBAL. No extra infrastructure to deploy, back up, or monitor.

Do I have to event-source my whole application?

No. You can adopt event sourcing per aggregate, exactly where the business value lives, whether that is orders, bookings, or payments, and keep simple CRUD for the rest. The library coexists peacefully with Doctrine ORM, Eloquent, and existing code.

What happens when my events need to change?

Software evolves, and so do events. Upcasters transform old event payloads on the fly when they are loaded, so you never have to migrate the event store itself. Your history stays immutable while your code moves forward.

How stable is the library?

Very. We are 100% committed to semantic versioning: no breaking changes within a major version, ever. Deprecations are announced ahead of time and every upgrade path is documented, so updating stays painless. And you are not on your own. We actively maintain the library and provide support on GitHub, with professional support available if you need more.

What if my framework is not supported?

No problem. The core library is plain PHP with no framework dependency, so it works in any project, whether that is Slim, Laminas, CodeIgniter, or no framework at all. If you use Symfony or Laravel, official integrations give you a head start: a Symfony bundle with autowiring, Messenger, and Doctrine migrations, and a Laravel package with auto-discovery and Artisan commands.

Where do I get help?

Start with the documentation. It covers everything from your first aggregate to advanced topics like snapshots and subscriptions. For questions and bug reports, the team is active on GitHub. And if you need deeper support, patchlevel offers consulting, code reviews, and workshops.

Ready to build?

Get started in five minutes. The docs walk you through your first aggregate, event, and projection.

MIT
Free forever, no paid tiers
No new infra
Runs on the database you already have