Skip to main content

Storage contracts and adapters

The core package does not select a database. A persistent adapter implements the interfaces in DomainFlow\EventSourcing\Interface and is passed to the facade.

Event storage

The central contract is EventStorageInterface:

public function storeEvents(array $events): void;
public function retrieveEvents(EntityIdentifierInterface $aggregateId): array;
public function retrieveEventsFromVersion(
EntityIdentifierInterface $aggregateId,
EventVersion $afterVersion,
): array;
public function retrieveAllEvents(): iterable;
public function deleteEvents(EntityIdentifierInterface $aggregateId): void;
public function retrieveEventsFromPosition(?string $afterPosition, int $limit): GlobalEventPage;
public function retrievePaginatedEvents(?int $offset, ?int $limit): array;
public function getCurrentMaxVersion(EntityIdentifierInterface $aggregateId): EventVersion;

storeEvents() stores a batch atomically from the contract's point of view and rejects duplicate stream versions with ConcurrencyException. The global position methods are intended for cross-aggregate readers such as projectors. The position is an opaque adapter-defined string; persist it and pass it back without parsing it.

retrievePaginatedEvents() is retained for compatibility but offset pagination over a growing stream can skip or repeat events. Use retrieveEventsFromPosition() with CatchUpReader for a resumable global read.

Entry factories

An adapter usually converts between its row/document representation and EventPersistenceRecord. It can provide an EventEntryFactoryInterface to InMemoryEventStorage or to its own storage implementation. The default DefaultEventEntryFactory serializes the event payload as JSON and stores metadata beside the payload.

The factory seam also supports custom event factories, upcasters, event type registries, and custom database fields. Adapter-specific row mapping belongs in the adapter, not in the domain event.

Reference implementation

InMemoryEventStorage is useful for tests and examples. It keeps one in-memory global log, retrieves aggregate streams in version order, and provides global positions for the lifetime of the object. It is not durable and does not replace a production adapter.

Adapter contract tests

The package ships reusable PHPUnit contract test cases under provider/Unit and provider/Integration. An adapter package can require this package and extend the relevant abstract test case for its concrete storage. This keeps the adapter tests aligned with the same behavior exercised by the in-memory reference implementations.

Concurrency checking

Concurrency checking is opt-in at the facade:

use DomainFlow\EventSourcing\Concurrency\MaxVersionStrategy;

$facade->enableConcurrencyCheck(new MaxVersionStrategy());

MaxVersionStrategy checks that each batch continues every aggregate stream in a contiguous run. A conflicting or gapped version raises ConcurrencyException before the inner storage is called.

The concrete adapters have separate documentation: