Aggregate Event Streams for Event Sourcing
An aggregate stream is the ordered history for one consistency boundary, such as an order, account, or workflow. Give every aggregate a stable entity ID, append only facts owned by that boundary, and derive current state by folding its events in version order.
Problem
Why this pattern exists
Streams become hard to reason about when they mirror database tables or UI screens instead of domain ownership. A single customer-wide stream creates needless write contention; one stream per field destroys the ability to enforce invariants. The useful boundary is the smallest unit that must accept or reject a command consistently.
Identity must survive renames and presentation changes. Prefer an opaque domain identifier such as order-01J... over an email address or display label. Put correlation IDs, causation IDs, actor identity, and source-system details in metadata so workflows can be traced without mixing transport concerns into business payloads.
Design decisions
Make boundaries explicit
- 01
Choose a consistency boundary
Group events that must be validated together. Separate histories that can progress independently, even when a read model later joins them. This keeps optimistic-concurrency conflicts meaningful instead of turning unrelated activity into one hot stream.
- 02
Name facts, not commands
Use past-tense event types such as order.placed and payment.captured. Events describe accepted facts. A rejected command belongs in an operational record only when rejection itself is a domain fact worth retaining.
- 03
Keep stream order authoritative
Treat aggregate version as the ordering signal. Wall-clock timestamps help operators and temporal queries, but clocks can collide or skew and should not replace stream version for state reconstruction.
AllSource implementation
Apply pattern to durable Core history
In AllSource Core, entity_id identifies the aggregate stream and each accepted event receives a version. Write structured facts through POST /api/v1/events, then query the entity history in ascending order. Query Service can fold that tenant-scoped history into current-state views without changing Core's durable record.
Start with one event type namespace per bounded context, for example order.*, shipment.*, and payment.*. Register payload schemas before producers multiply. When a process spans streams, keep correlation metadata across events and let a workflow projection show the joined story; do not collapse separate consistency boundaries into one stream for reporting convenience.
order.placed v1 { total: 14999, currency: "GBP" }
order.line_added v2 { sku: "CHAIR-01", quantity: 2 }
order.confirmed v3 { confirmed_by: "customer" }
order.dispatched v4 { carrier: "DPD", tracking_ref: "..." }Failure modes
Detect weak implementations early
One tenant or customer stream receives every write and conflicts constantly.
Fix: Split by real aggregate identity; retain tenant ID as scope, not aggregate boundary.
Events say update_order or set_status and cannot explain what happened.
Fix: Rename accepted outcomes as stable past-tense domain facts.
A renamed email or slug creates a second history for the same entity.
Fix: Use an immutable entity ID and keep mutable labels inside events or projections.
Production checklist
Ready when each statement is true
- Aggregate owns one enforceable set of invariants.
- Entity ID remains stable for lifetime of history.
- Event types describe accepted facts in past tense.
- Version, correlation, causation, actor, and tenant context are retained.
- Cross-aggregate reporting happens in projections, not write streams.
Related patterns
Continue through adjacent decisions
Optimistic concurrency
Optimistic concurrency protects a stream by accepting a write only when its expected version matches the current version. A mismatch means another command changed the aggregate first, so the caller must reload events, re-evaluate the command, and either append a new valid event or report a domain conflict.
Read nextProjections and read models
A projection folds ordered events into a query-specific read model. It is derived state, not a second source of truth: operators must be able to discard it, replay source events, and build a replacement without rewriting event history or stopping existing readers.
Read nextMulti-tenant streams
A multi-tenant event store must enforce tenant scope before every read, write, replay, subscription, snapshot, and projection operation. Tenant identity is an authorization boundary carried independently from aggregate identity; matching entity IDs in two tenants must never share history or derived state.
Read nextStore history once
