Alpha Version: You are viewing the ALPHA documentation. This is an experimental version and may contain breaking changes.
Skip to main content

Aggregate vs DCB: Decision Guide

This guide helps developers (and AI skills) choose between the Aggregate approach, the DCB (Dynamic Consistency Boundary) approach, or a Hybrid mix within a Reventless plugin.


Quick Decision Table

QuestionAggregateDCB Slices
Does the entity have a self-contained lifecycle?Yes--
Do multiple entities share consistency rules?--Yes
Is the command validation logic isolated to one entity?Yes--
Must a command check state across multiple entity IDs?--Yes
Is the entity independent of other entities in the same plugin?Yes--
Does the entity exist mainly to sync external data?--Yes
Do you need automation, inbound/outbound translation slices?--Yes
Is the domain well-understood with clear entity boundaries?YesEither

"--" means the approach is not a natural fit but is not prohibited.


The Two Approaches

Aggregate Approach

An Aggregate is a write-side component with its own private event stream per entity instance. Each aggregate ID has its own event history, and state is reconstructed by replaying that history on every command.

Components:

  • Aggregate (Spec + Behavior) — handles commands, produces events
  • EventLog — one append-only stream per aggregate ID
  • ReadModel + Projections — query-side views consuming aggregate events

Command flow:

Command → CommandTopic (FIFO, ordered per ID)
→ replay(id) → evolve events into state
→ decide(state, command) → new events
→ append to EventLog → publish to EventTopic
→ ReadModel projects events into QueryDb

DCB Approach

A DCB uses a single shared event log per plugin. Commands are handled by StateChangeSlices that query the log using tags, build a minimal decision model, and append new events with optimistic concurrency.

Components:

  • StateChangeSlice — handles commands, queries shared event log, produces events
  • StateViewSlice — projects events into queryable state (read-side)
  • DcbEventLog — single shared event log with tag-based filtering
  • AutomationSlice — event-driven autonomous workflows
  • InboundTranslationSlice — converts external input to commands
  • OutboundTranslationSlice — reacts to events by calling external systems

Command flow:

Command → CommandTopic (shared, one per plugin)
→ extract tags from command
→ query DcbEventLog by tags + event types
→ evolve matching events into decision model
→ decide(state, command) → new events
→ conditional append (retry on conflict)
→ publish to EventTopic
→ StateViewSlice projects events into QueryDb

Hybrid Approach

A single plugin can mix both approaches. Independent entities use aggregates; interdependent entities share a DCB event log. Both participate in the same plugin assembly, share the same API, and can define extension points and extensions.


Decision Criteria

1. Entity Independence

Choose Aggregate when the entity has a self-contained lifecycle. Its commands only need the entity's own history to make decisions. No other entity's state is required at decision time.

Choose DCB when entities are interdependent. A command on entity A needs to check the state of entity B (or a set of entities) to decide whether to proceed.

Examples:

  • Aggregate: Category — adding, renaming, or archiving a category only depends on the category's own state (does it exist? is it already archived?).
  • DCB: PlaceOrder — placing an order must verify that all referenced products exist and are available, which requires reading CatalogProduct events alongside Order events.

2. Consistency Boundary Shape

Choose Aggregate when consistency boundaries align with entity identity. Every invariant you need to enforce involves exactly one entity ID.

Choose DCB when consistency boundaries span multiple entities or are determined at runtime. The set of events needed for a decision varies per command.

Examples:

  • Aggregate: "A product cannot be added twice" — one product ID, one check.
  • DCB: "An order can only be placed if all its line items reference existing products" — one order ID + N product IDs, checked atomically via tag-based query.

3. Command Granularity

Choose Aggregate when commands map naturally to a single entity with multiple operations. The aggregate accumulates rich state and each command variant operates on that state.

Choose DCB when commands are thin and focused. Each command does one thing, often involving a cross-entity check. DCB slices are naturally fine-grained — one slice per command type.

Examples:

  • Aggregate: Product with commands Add, UpdateName, UpdateDescription, UpdatePrice — all operating on the same entity state. A single behavior module handles all variants.
  • DCB: AddProduct, ChangeProductName, ChangeProductDescription, ChangeProductPrice — each is a separate StateChangeSlice. They share the event log but have independent decision logic.

4. External Data Synchronization

Choose DCB when an entity exists primarily to mirror data from another plugin or external system. DCB's StateChangeSlice + Extension pattern is purpose-built for this.

Examples:

  • DCB: SyncCatalogProduct — a StateChangeSlice in the Ordering plugin that receives product data from the Catalog plugin's extension point and writes it to the shared event log. The Ordering plugin's PlaceOrder slice can then query these synced product events alongside order events.
  • Aggregate: ProductDemand in the aggregate approach requires a dedicated aggregate with no user commands (extension-driven only), separate EventLog, separate ReadModel — more infrastructure for the same result.

5. Automation and Side Effects

Choose DCB when you need autonomous workflows or external system integration within the same bounded context.

DCB provides dedicated slice types:

  • AutomationSlice — reacts to events by generating new commands (e.g., auto-ship orders after payment confirmation)
  • OutboundTranslationSlice — reacts to events by calling external systems (e.g., send order confirmation email)
  • InboundTranslationSlice — receives external input and translates to commands (e.g., import products from supplier feed)

The aggregate approach handles these scenarios through Tasks (for long-running operations) and EventMappers (for aggregate-to-aggregate event routing), but the DCB slice types are more explicit and composable.

6. Event Log Size and Query Performance

Choose Aggregate when you expect high event volumes per entity. Each aggregate has its own stream — replay cost scales with the entity's history, not the entire domain.

Choose DCB when individual entities have modest event counts but you need flexible cross-entity queries. All events share one log, filtered by tags.

Trade-offs:

  • Aggregate replay is fast for small streams (< 1000 events per ID) but grows linearly. Snapshots can mitigate this.
  • DCB queries use DynamoDB secondary indexes indexed by tag values. Cross-entity queries are efficient but all events compete for the same table throughput.

Advantages and Consequences

Aggregate Approach

AdvantageConsequence
Clear entity boundaries — one aggregate, one event stream, one behaviorMore infrastructure per entity (CommandTopic, EventLog, EventTopic per aggregate)
Strong consistency within entity — commands serialized by IDNo cross-entity transactions — must use sagas or extension-driven aggregates for coordination
Full audit trail per entity — replay any ID to any point in timeState reconstruction cost — every command replays the full event history
Isolated testing — BehaviorTest DSL tests decide/evolve in pure functionsSeparate ReadModel infrastructure required for every query view
Mature pattern — well-documented, widely understoodIdempotency must be explicit (Ok([]) for no-change commands)
Efficient per-ID storage — DynamoDB partition per aggregate IDEvent schema is per-aggregate — no shared type across entities

DCB Approach

AdvantageConsequence
Shared event log — one DcbEventLog per plugin, simpler infrastructureAll events share one table — throughput planning is domain-wide
Dynamic consistency — tag-based queries determine boundaries at runtimeTag annotations required on all filterable fields (@s.matches(DcbTag.string))
Cross-entity decisions — one query can span multiple entity typesOptimistic concurrency with retries (up to 3) — conflicts are expected
Fine-grained slices — one slice per command, easy to add/removeMore files per domain concept (one slice file per command vs one behavior with all commands)
Built-in automation and translation slicesDCB-specific slice types only work with DcbEventLog, not aggregates
Minimal decision model — evolve only the fields needed for the decisionEvent type union grows as slices are added (all events in one schema)
No separate ReadModel wiring — StateViewSlice projects directlyStateViewSlice is simpler but less flexible than ReadModel (single event source)

Hybrid Approach

AdvantageConsequence
Best of both — independent entities get aggregates, interdependent entities share DCBTwo mental models in one plugin — team must understand both
Natural domain decomposition — each entity uses its natural boundary stylePlugin assembly is larger (both aggregate and DCB parameters)
Incremental migration — start with aggregates, introduce DCB where neededMust choose per entity — no entity can be both

Use Cases

Use Case 1: Product Catalog (Aggregate)

A catalog where products have independent lifecycles. Adding a product only checks if that product ID already exists. Updating a product only needs the product's current state.

Why Aggregate: Each product is fully self-contained. No product command needs another product's state. Clear entity boundary = clear aggregate.

Aggregate: Product (Add, UpdateName, UpdatePrice, Archive)
ReadModel: ProductsView (projects product events)

Use Case 2: Order Placement with Inventory Check (DCB)

An ordering system where placing an order must verify that all referenced products exist and are available. The decision spans order state + product catalog state.

Why DCB: The PlaceOrder command needs to read product availability events alongside order events. A tag-based query on productId pulls events from both SyncCatalogProduct and PlaceOrder slices atomically.

StateChangeSlice: PlaceOrder (queries product + order events)
StateChangeSlice: SyncCatalogProduct (mirrors catalog data)
StateViewSlice: OrdersView, AvailableProductsView
AutomationSlice: AutoShipOrder (auto-ships after payment)
OutboundTranslationSlice: SendOrderConfirmation (emails)

Use Case 3: Category Management (Aggregate in Hybrid)

Categories in a catalog have a simple, independent lifecycle alongside a DCB-based product system.

Why Aggregate in a Hybrid plugin: Category has no dependency on products or other entities. It doesn't need cross-entity consistency. Making it an aggregate keeps it simple while the more complex product/demand domain uses DCB.

Plugin: Catalog
Aggregate: Category (Add, Rename, Archive)
ReadModel: CategoriesView
DCB: Product slices, ProductDemand slices (shared event log)

Use Case 4: Customer Management (Aggregate)

Customers register, update profile fields, and deactivate. Each operation depends only on the customer's own state.

Why Aggregate: Classic self-contained entity. No cross-customer invariants. Registration checks if the customer exists; updates check current state; deactivation is idempotent.

Aggregate: Customer (Register, UpdateEmail, UpdateAddress, Deactivate)
ReadModel: CustomersView

Use Case 5: Reservation System with Capacity Limits (DCB)

A venue booking system where reserving a seat must check remaining capacity across all existing reservations for the same event.

Why DCB: The "reserve seat" command must count existing reservations for the event (cross-entity query by eventId tag) and compare against venue capacity. The consistency boundary spans all reservations for one event.

StateChangeSlice: ReserveSeat (queries reservations by eventId tag)
StateChangeSlice: CancelReservation
StateViewSlice: EventAvailabilityView
StateViewSlice: ReservationsView

Use Case 6: Supplier Product Import (DCB with InboundTranslation)

An external supplier feed provides product data that must be validated and imported into the catalog.

Why DCB: InboundTranslationSlice receives external JSON, validates it (currency, required fields), and translates to AddProduct commands. This is a DCB-native pattern with no aggregate equivalent.

InboundTranslationSlice: ImportProduct (validates + translates)
StateChangeSlice: AddProduct (handles the generated command)

Use Case 7: Audit Log / Compliance Tracking (Aggregate)

A compliance system where every entity action must be independently auditable with full replay capability.

Why Aggregate: Per-entity event streams provide a complete audit trail. You can replay any entity to any point in time. The isolation guarantee means one entity's events never mix with another's.


Decision Flowchart

Does the command need state from multiple entity types?
├─ Yes → DCB
└─ No

Does the entity have a self-contained lifecycle?
├─ Yes → Aggregate
└─ No (e.g., sync-only, reactive)

Is it part of a plugin that already uses DCB?
├─ Yes → DCB (add as another slice)
└─ No → Consider DCB or extension-driven Aggregate

Mixing Approaches: Guidelines

  1. Per-entity decision. Each entity in a plugin is either an aggregate or a set of DCB slices. Never both.

  2. Group interdependent entities in DCB. If entities A and B share consistency rules, both must be DCB slices on the same DcbEventLog.

  3. Keep independent entities as aggregates. If an entity's commands never need another entity's state, an aggregate is simpler and more isolated.

  4. Extension points work with both. Both aggregates and DCB slices can define extension points and extensions. The mapping layer abstracts the internal approach.

  5. One DcbEventLog per plugin. All DCB slices in a plugin share a single event log. You cannot have multiple DcbEventLogs in one plugin.

  6. Start with aggregates, introduce DCB when needed. If you're unsure, start with aggregates. When you hit a cross-entity consistency requirement, convert those entities to DCB slices. The hybrid approach makes this migration incremental.


Reference: Component Mapping

Aggregate ApproachDCB ApproachPurpose
Aggregate (Spec + Behavior)StateChangeSliceCommand handling, event production
EventLog (per ID)DcbEventLog (shared, tag-filtered)Event storage
ReadModel + ProjectionsStateViewSliceQuery-side projections
EventMapperAutomationSliceEvent-driven command generation
TaskInboundTranslationSliceExternal data ingestion
Side Effect HandlerOutboundTranslationSliceExternal system integration
CommandTopic (per aggregate)CommandTopic (shared per plugin)Command delivery
EventTopic (per aggregate)EventTopic (shared)Event distribution

For AI Skills: Structured Decision Input

When an AI skill needs to decide the approach for a new entity, evaluate these properties:

entity:
name: string
commands: array<{name, fields, crossEntityChecks: bool}>
events: array<{name, fields}>
dependencies:
- needsStateFrom: array<entityName> # other entities whose state is needed at decision time
- syncedFrom: option<extensionPoint> # mirrors data from another plugin
- automationTriggers: array<eventName> # events that trigger autonomous commands
- externalIntegration: {inbound: bool, outbound: bool}

Decision rules:

  1. If crossEntityChecks = true for any command, or needsStateFrom is non-empty → DCB
  2. If syncedFrom is Some → DCB (StateChangeSlice receiving extension events)
  3. If automationTriggers is non-empty → DCB (with AutomationSlice)
  4. If externalIntegration.inbound = trueDCB (with InboundTranslationSlice)
  5. If externalIntegration.outbound = trueDCB (with OutboundTranslationSlice)
  6. If none of the above → Aggregate
  7. If the entity is in a plugin where related entities are already DCB → prefer DCB for cohesion
  8. Group all DCB entities in the same plugin onto the same DcbEventLog

Output: For each entity, emit either:

  • {approach: "aggregate", spec: AggregateSpec, behavior: Behavior, readModels: [...]}
  • {approach: "dcb", slices: {stateChange: [...], stateView: [...], automation?: [...], inbound?: [...], outbound?: [...]}}