Dynamic Consistency Boundaries (DCBs)¶
Event-sourced systems often use aggregates to define where consistency must be preserved. Aggregates help prevent conflicting updates and capture business invariants. But they also introduce rigidity: Once an aggregate boundary is defined, it becomes difficult to change – even when the domain evolves or the original design turns out to be insufficient.
Dynamic Consistency Boundaries (DCBs) offer a more flexible alternative. Instead of defining consistency boundaries upfront in the domain model, DCBs let you enforce consistency at runtime, based on the actual state of the event store and the specific decision you're about to make. The key idea is simple: Consistency becomes a condition, not a structure.
How It Works¶
DCBs rely on EventQL queries to express what consistency means in a given context. The process typically follows three steps:
- You start by running an EventQL query that selects the set of events relevant for the current decision. This is effectively an ad-hoc aggregate: it groups just the data you need, for just this one decision.
- Based on the selected events, you apply business logic in your application. For example, you might check whether a reader is allowed to borrow another book.
- When you write the resulting event, you include a precondition of type
isEventQlQueryTrueto re-evaluate the query and ensure that the same invariant still holds – that is, that nothing has changed since the decision was made.
This pattern allows you to define consistency dynamically, depending on event content, history, or relationships – without requiring fixed subject boundaries or predefined aggregates.
Here's an example:
{
"type": "isEventQlQueryTrue",
"payload": {
"query": "FROM e IN events WHERE e.type == 'io.eventsourcingdb.library.book-borrowed' AND e.data.borrowedBy == '/readers/23' PROJECT INTO COUNT() < 3"
}
}
This precondition ensures that the reader identified by /readers/23 has borrowed fewer than three books. If they've already borrowed three or more, the write is rejected.
Simplified Example
This is a simplified example: it doesn't account for returned books or due dates. But it shows the general idea – the consistency rule (invariant) is enforced dynamically, based on the reader's current borrowing history. The same query must be run twice: first to make the decision (can this book be borrowed?), and again at write time to confirm that the underlying event set hasn't changed.
Trade-offs and Considerations¶
DCBs offer flexibility – but not for free. Unlike subject-based reads, EventQL queries are more expensive to evaluate, especially when scanning large datasets. And since DCBs require two query passes – one during decision-making, and one as a precondition – the cost effectively doubles.
For this reason, DCBs are best used when:
- Static aggregates would be too coarse or too inflexible,
- The consistency rules change over time or depend on data content,
- Or the relevant events span multiple subjects or types.
In such cases, DCBs can reduce complexity, improve expressiveness, and avoid workarounds like synthetic aggregates or distributed locks. But in performance-sensitive paths, traditional subject-based aggregates may still be the better fit.
Aggregates and DCBs in Context¶
DCBs and aggregates are complementary tools. Aggregates define design-time consistency boundaries that reflect stable, subject-local rules. DCBs define runtime consistency conditions that adapt to the actual structure of decisions and data.
You can – and often should – use both:
- Use aggregates when you can cleanly define them, and when invariants are naturally tied to subjects.
- Use DCBs when consistency depends on a broader context, or when you want to enforce rules declaratively based on event history.
This flexibility lets you evolve your consistency model gradually – without locking yourself into rigid structures too early.
Further Reading¶
The concept of dynamic consistency boundaries is discussed in more depth at dcb.events. EventSourcingDB supports this pattern natively via the isEventQlQueryTrue precondition.