Dynamic Consistency Boundaries
A Dynamic Consistency Boundary (DCB) lets a write stay consistent with exactly the events its decision depended on, across as many entities as it needs, without locks, distributed transactions or sagas. The idea is formalized at dcb.events. EvidentSource implements it with append conditions.
The problem
Section titled “The problem”Classic event sourcing works well when every command touches one entity. It stops working neatly when a decision depends on several: “approve this transfer only if neither account changed since I checked them.”
The traditional answers are distributed transactions (slow and lock-prone), sagas (eventual consistency and compensation logic), or aggregates grown large enough to hold everything they might need.
The DCB answer
Section titled “The DCB answer”A transaction carries append conditions. Each one names an event selector, and checks the latest event matching it at the moment of commit. If any condition fails, nothing commits.
So a decision says what it read, and the append says: only if that is still true.
What a condition tests
Section titled “What a condition tests”Each condition names a selector s, over stream, subject and event type, and tests L(s): the revision of the latest event matching s, or 0 if nothing matches.
| Condition | Holds when |
|---|---|
Min(s, r) | L(s) ≥ r |
Max(s, r) | L(s) ≤ r |
Range(s, a, b) | a ≤ L(s) ≤ b |
The bounds are revisions, not counts of events. A condition can’t count matches, and no selector can refer to time.
Three patterns
Section titled “Three patterns”| Goal | Condition | Why |
|---|---|---|
| Nothing matches yet | Max(s, 0) | L(s) is 0 only when nothing matches. This is the DCB spec’s failIfEventsMatch. |
| Something matches | Min(s, 1) | Any match puts L(s) at 1 or higher. |
| Nothing changed since you read | Max(s, r), with r the revision you read at | A later matching event pushes L(s) past r. |
Use Max for concurrency, never Min. L(s) only moves forward, so once L(s) ≥ r holds it keeps holding, whatever else commits.
An example
Section titled “An example”A transfer that must not commit if either account changed after the revision the decision read, 42:
{ "events": [{"specversion": "1.0", "id": "t-1", "source": "transfers", "type": "TransferApproved", "subject": "transfer-9"}], "conditions": [ {"max": {"selector": {"equals": {"subject": {"hasValue": true, "value": "account-A"}}}, "revision": 42}}, {"max": {"selector": {"equals": {"subject": {"hasValue": true, "value": "account-B"}}}, "revision": 42}} ]}A State Change’s decide returns the same conditions alongside its events. The SDKs name the common cases; in Rust and Python they are must_not_exist, must_exist, unchanged_since and at_revision.
When a condition fails
Section titled “When a condition fails”The whole transaction fails: HTTP 409, or gRPC FAILED_PRECONDITION. Nothing commits. Read again at the latest revision, decide again, and send the transaction with conditions against the new revision.
Retrying is safe. A transaction carries an id, and resending the same id returns the result already recorded for it, instead of committing twice.
Three things to know
Section titled “Three things to know”- The server adds no conditions of its own. A decision is protected only by the conditions it sends.
- One check the server does add is not a condition. If the database a decision read was deleted and the name created again before the transaction reached the writer, it is refused with HTTP
409, or gRPCFAILED_PRECONDITION. It never looks at revisions, so no condition could express it. - Two conditions on the same selector are merged, and the merge can loosen them. A
Minand aMaxcombine into a range, and one that can never hold is refused with400; any other pair keeps the looser bound. Send each check under its own selector, and don’t rely on two bounds over one selector holding together.
When to reach for DCB
Section titled “When to reach for DCB”Whenever a command’s correctness depends on state you read, and that state could change between your read and your append. Conditions are how a decision says: “This was decided assuming X. If X isn’t still true, don’t apply it.”