noddde

Why Two Persistence Strategies?

Why noddde supports both event sourcing and state storage for aggregate persistence.

The Decision

noddde provides two persistence interfaces — EventSourcedAggregatePersistence (store events) and StateStoredAggregatePersistence (store state) — and lets you choose at configuration time.

The Problem

Event sourcing is powerful but not always appropriate. Forcing every aggregate to use event sourcing adds complexity where simple state storage would suffice:

  • A user profile that only updates name and email does not need an event stream
  • A simple settings aggregate does not benefit from temporal queries
  • Some teams are not ready for the operational overhead of event stores

On the other hand, state-stored-only frameworks cannot provide audit trails, event replay, or projection rebuilding.

Alternatives Considered

  • Event sourcing only — Like Axon Framework. Every aggregate is event-sourced.
  • State storage only — Like traditional ORMs. No event history.
  • Hybrid per-aggregate — Different persistence strategy per aggregate type.

Why This Approach

The same aggregate definition works with either strategy:

// This aggregate works with EITHER persistence strategy
export const BankAccount = defineAggregate<BankAccountDef>({
  initialState: { ... },
  commands: { ... },
  apply: { ... },
});

The choice is made at configuration time:

// Event sourcing — store events, replay on load
infrastructure: {
  aggregatePersistence: () => new InMemoryEventSourcedAggregatePersistence(),
},

// OR state storage — store final state, load directly
infrastructure: {
  aggregatePersistence: () => new InMemoryStateStoredAggregatePersistence(),
},

Benefits:

  • Start simple — Use state storage initially, migrate to event sourcing when you need it
  • Per-environment — Use event sourcing in production, state storage in tests for speed
  • Same aggregate — No code changes needed when switching strategies
  • Gradual adoption — Teams can adopt event sourcing incrementally

Trade-offs

  • Two interfaces — Two persistence contracts to understand
  • Two mental models — Developers need to understand when each is appropriate
  • Apply handlers still required — Even with state storage, you define apply handlers (they are used to process events returned by command handlers within a single dispatch cycle)

When to Use Each

Use Event Sourcing When...Use State Storage When...
You need an audit trailYou only need current state
You want temporal queriesYou have simple CRUD operations
You plan to rebuild projectionsStorage growth is a concern
Events drive other systemsSimplicity is the priority
You need debugging capabilitiesPerformance is critical (O(1) load)

Example

// Development: in-memory event sourcing for debugging
const devDomain = await configureDomain<MyInfra>({
  /* same writeModel and readModel */
  infrastructure: {
    aggregatePersistence: () => new InMemoryEventSourcedAggregatePersistence(),
  },
});

// Production: PostgreSQL event store for full audit trail
const prodDomain = await configureDomain<MyInfra>({
  /* same writeModel and readModel */
  infrastructure: {
    aggregatePersistence: () => new PostgresEventStore(connectionString),
  },
});

Same aggregates, same projections, different persistence. The aggregate code does not change.

On this page