replay 1.1.0 copy "replay: ^1.1.0" to clipboard
replay: ^1.1.0 copied to clipboard

Event Sourcing framework for Dart to build auditable apps with immutable events.

Replay #

Event Sourcing framework for Dart to build auditable apps with immutable events.

Why Event Sourcing? #

Event sourcing is the pinnacle of event driven architectures!

It records all changes to an application's state as a sequence of immutable events, rather than storing only the current state.

This provides you with a complete audit log by design and enables temporal queries ("show me last Tuesday's state") by allowing you to deterministically replay past events.

Event Sourcing powers undo/redo in apps, audit trails in banking, and Git’s commit history.

Installation #

dart pub add replay

Features #

✔️ Idempotent event processing
✔️ Snapshotting support
✔️ In-memory or custom event storage support
🔜 Automatic partitioning (WIP)
✔️ Sound null safety
✔️ 100 % test coverage
✔️ No dependencies

Usage #

See example.

Start by defining immutable classes for:

  • the state of an aggregate
  • events with a common interface
  • commands with a common interface

Then create classes that implement CommandDecider and EventReducer.

Finally put it all together in one Aggregate and call process on every command:

import 'package:replay/replay.dart';

final aggregate = Aggregate<BankCommand, BankEvent, BankState>(
  initialState: BankState(),
  commandDecider: ComposableCommandDecider({
    OpenAccountCommand: OpenAccountCommandDecider(),
    CloseAccountCommand: CloseAccountCommandDecider(),
    TransferMoneyCommand: TransferMoneyCommandDecider(),
  }),
  eventReducer: ComposableEventReducer({
    BalanceSetEvent: BalanceSetEventReducer(),
    BalanceUnsetEvent: BalanceUnsetEventReducer(),
  }),
  eventStorage: InMemoryEventStorage(),
);

aggregate.process(OpenAccountCommand(accountName: 'Foo', initialBalance: 100));

Concepts #

Commands vs. Events #

Both commands and events describe actions from a business perspective, but that's where their similarities end.

Commands:

  • Request an action in imperative mood ("send message")
  • Originate externally
  • Immutable once issued
  • Can be invalid/rejected (require validation)
  • May produce 0-N events when processed

Events:

  • Record completed actions in past tense ("message sent")
  • Internally-generated facts
  • Always immutable (cannot be modified) & append-only
  • Can never be invalid
  • Used to construct the application state (single source of truth)
    • Use "snapshots" of the state for optimized queries (Command Query Responsibility Segregation - CQRS)
  • Idempotent: Processing the same event repeatedly always results in the same state
  • Atomic: Either processing succeeds completely or nothing changes

Aggregate #

An aggregate acts as a consistency boundary — grouping a command's events into atomic (all-or-nothing) operations. It enforces invariants by validating commands against the state, which is derived from past events. Valid commands produce new events, updating the aggregate. These events are stored immutably to enable auditability.

Options: Discovering Valid Commands #

Sometimes you don’t want to guess whether a command will succeed — you want to know all valid commands, given the current state.
This is addressed via a unique (but optional) concept: Options.

Options represent potential commands that could be processed for the current state of an aggregate.
Just as events are derived from commands, commands are derived from options.
Unlike commands, options stem from inspecting the aggregate's state rather than being issued externally.
A single option can represent multiple (potentially infinitely many) commands by specifying the constraints that all commands of that type must satisfy.

To implement this, create an OptionFinder for your aggregate. It examines the current state and returns all options:

final aggregate = AggregateFullyGeneric<BankCommand, BankEvent, BankState, BankOption>(
  ...
  optionFinder: ComposableOptionFinder({
    OpenAccountOption: OpenAccountOptionFinder(),
    CloseAccountOption: CloseAccountOptionFinder(),
    TransferMoneyOption: TransferMoneyOptionFinder(),
  }),
);

final options = aggregate.findOptions();

This enables automated systems to query all valid commands, empowering UIs where every offered action is guaranteed to succeed.

0
likes
160
points
228
downloads

Publisher

unverified uploader

Weekly Downloads

Event Sourcing framework for Dart to build auditable apps with immutable events.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

More

Packages that depend on replay