Skip to content
Architecture Overview

Architecture Overview

This page is a high-level map of Estoria: how its components fit together, which one owns which responsibility, and where the extension points are.

Core Concepts

Estoria maps event sourcing concepts onto a small set of Go types:

  • Entity state — a plain struct of your own (Account, Order, …) plus a factory function that produces its initial state. Estoria imposes no interface on it.
  • estoria.DomainEvent[S] — a state-changing event. ApplyTo(state S) S is total: a persisted event is a fact, and applying one cannot fail.
  • eventstore.Store — persistent storage for event streams (StreamReader + StreamWriter).
  • aggregatestore.Store[S] — the high-level interface applications use to create, load, and save aggregates.
  • aggregatestore.New(...) — constructs the event-sourced store, the primary implementation, which reconstructs aggregates by replaying their events.

Component Relationships

┌──────────────────────────────────────────────────────────┐
│                    Application Layer                     │
│             (commands, queries, domain logic)            │
└─────────────┬────────────────────────────────────────────┘
              │  New / Load / Save
┌─────────────▼────────────────────────────────────────────┐
│              Aggregate Store  (Store[S])                 │
│                                                          │
│   optional decorators, stackable in any order:           │
│   ┌────────────┐  ┌────────────┐  ┌────────────┐         │
│   │Snapshotting│  │   Cached   │  │ App-owned  │         │
│   └────────────┘  └────────────┘  └────────────┘         │
│                                                          │
│   core implementation:                                   │
│   ┌──────────────────────────────────────────────┐       │
│   │  Event-sourced store (aggregatestore.New)    │       │
│   │  • replays events to load aggregates         │       │
│   │  • appends events to save them               │       │
│   │  • owns domain event <-> bytes (codec)       │       │
│   │  • owns the aggregate type name              │       │
│   └──────────────────────────────────────────────┘       │
└─────────────┬────────────────────────────────────────────┘
              │  ReadStream / AppendStream
┌─────────────▼────────────────────────────────────────────┐
│                Event Store  (eventstore.Store)           │
└─────────────┬────────────────────────────────────────────┘
              │
   ┌──────────┼──────────┬───────────┬───────────┐
   │          │          │           │           │
┌──▼─────┐ ┌──▼───┐ ┌────▼────┐ ┌────▼─────┐ ┌───▼────┐
│Postgres│ │SQLite│ │ MongoDB │ │KurrentDB │ │ Memory │
└────────┘ └──────┘ └─────────┘ └──────────┘ └────────┘

Data Flow

Read Path: Loading an Aggregate

  1. The application calls store.Load(ctx, id, nil).
  2. The store composes the aggregate’s typed ID from its configured type name and the given UUID, creates the initial state with the factory, and hydrates it.
  3. Hydration calls eventStore.ReadStream(...) and, for each event, decodes the payload with the store’s DomainEventCodec and applies it with ApplyTo, advancing the aggregate’s version.

With decorators in the stack: a snapshotting store first loads the latest snapshot and replays only the events after it; a cached store returns a cache hit without touching the event store at all.

Write Path: Saving an Aggregate

  1. The application queues events on the aggregate with Append (nothing is persisted or applied yet).
  2. store.Save(ctx, aggregate, nil) encodes each queued event with the DomainEventCodec and calls eventStore.AppendStream(...), which enforces optimistic concurrency against the aggregate’s loaded version.
  3. AppendStream returns the written events exactly as a subsequent read would — store-assigned IDs, versions, timestamps, and global positions — and the store applies them to the in-memory state.

With decorators: a snapshotting store consults its policy after the save and captures a snapshot when due; a cached store refreshes its entry.

Ownership Rules

Estoria assigns each serialization and naming fact exactly one owner:

  • Domain events → bytes is owned by the aggregate store, through its estoria.DomainEventCodec[S] (JSON by default, overridable with WithDomainEventCodec). Event store backends persist those bytes verbatim, alongside the codec’s declared content type — no backend re-encodes a payload it was handed.
  • Entity state → bytes (for snapshots and caches) is owned by the component doing the storing, through an estoria.StateCodec[S] (JSON by default).
  • The aggregate type name is owned by the aggregate store, supplied once at construction (aggregatestore.New(eventStore, "account", NewAccount, ...)). It becomes the type component of every aggregate ID the store composes, which is how streams are addressed in storage.

Store Decorators

Decorators wrap any aggregatestore.Store[S] and can be stacked:

  • Snapshotting — loads state from snapshots instead of full replay; captures snapshots per a SnapshotPolicy.
  • Cached — serves loads from an AggregateCache[S] backend, falling back to the inner store on a miss.
store, _ := aggregatestore.New(eventStore, "account", NewAccount,
    aggregatestore.WithEventTypes(AccountOpened{}, FundsDeposited{}))

cached, _ := aggregatestore.NewCachedStore(store, cache)
snapshotting, _ := aggregatestore.NewSnapshottingStore(cached, snapshotStore,
    snapshotstore.EventCountSnapshotPolicy{N: 100})

Application-owned behavior — reacting to saves, injecting ambient metadata — composes the same way: write a small decorator over any store in the stack.

Extension Points

Vendor-specific implementations live in estoria-contrib; the Component Library catalogs them.

  • Event stores implement eventstore.Store: PostgreSQL, SQLite, MongoDB, KurrentDB. The core library ships an in-memory store for testing. Optional capabilities — global reads and stream deletion — are discovered by type assertion.
  • Snapshot stores implement snapshotstore.SnapshotStore. Both current implementations are in core: snapshotstore/memory and snapshotstore/eventstream (snapshots stored as events).
  • Aggregate caches implement aggregatestore.AggregateCache[S]: Redis, Valkey, Freecache, BigCache.
  • Transactional outboxes ride the PostgreSQL and MongoDB event stores’ transaction hooks for reliable delivery to external consumers.
  • Checkpoint stores implement checkpointstore.Store, persisting projection progress: PostgreSQL, SQLite, MongoDB. Core ships checkpointstore/memory; custom implementations over the read model’s own database are verified by the checkpointstore/storetest suite.
  • Cutover setters and routers implement lifecycle.CutoverSetter and lifecycle.Router, converging read routing on recorded promotions and rollbacks.
  • Telemetry wrappers (OpenTelemetry, Datadog) instrument event, aggregate, and snapshot stores without changing their behavior.

Acceptance suites in eventstore/storetest, snapshotstore/storetest, and checkpointstore/storetest verify implementations against the behaviors Estoria’s components rely on; every contrib backend runs them in CI.

Key Interfaces

InterfacePurposeKey methods
estoria.DomainEvent[S]State-changing eventEventType(), New(), ApplyTo(S) S
estoria.DomainEventCodec[S]Domain event serializationMarshalDomainEvent, UnmarshalDomainEvent, ContentType
estoria.StateCodec[S]Entity state serializationMarshalState, UnmarshalState, ContentType
eventstore.StoreEvent persistenceReadStream, AppendStream
eventstore.StreamIteratorEvent stream traversalNext, Close
aggregatestore.Store[S]Aggregate operationsNew, Load, Hydrate, Save
aggregatestore.AggregateCache[S]Aggregate cachingGetAggregate, PutAggregate
snapshotstore.SnapshotStoreSnapshot persistenceReadSnapshot, WriteSnapshot
projection.EventHandlerRead-model event handlingHandle
checkpointstore.StoreProjection progressLoad, Save, Delete
lifecycle.RouterLive-version routingLive

Further Reading

  • Getting Started — build a first Estoria application
  • Components — detailed documentation for each component
  • Integrations — setup guides for the contrib backends
  • CQRS — read models, projections, and the transactional outbox
  • Projection Lifecycle — versioned read-model rebuilds, cutover, and retirement