36. Transactions inside one database and consistency across services
Answer as submitted
“Yes — I handle both scopes routinely and I am explicit about where each belongs.”
Expanded answer
Yes, and the two scopes are not alternatives: they are the two sides of one boundary, which is the transaction rather than the diagram. Inside one database a use case gets a real ACID boundary: one commit per use case, an explicit transaction only where the work spans several statements or aggregates, and EF Core's DbContext or Spring's @Transactional doing the work. The isolation level is chosen, not defaulted: read committed as the baseline because it is the level whose costs I can describe, snapshot where a multi-aggregate write or a long read needs a stable view and blocking writers would be worse, and serializable only where an invariant spans rows — always shipped with the retry policy its failure mode requires. In-process consistency is enforced by things that hold under concurrency rather than by things that read well: optimistic concurrency on a row version, a unique constraint as the last line of defence even when the code already checks, deadlock-aware retries with jitter, and transactions kept short so locks are released quickly. Across services the guarantee changes from atomic to convergent, so the mechanisms change with it: the outbox makes the state change and its event atomic in one local transaction, a saga rolls a business process back through compensations instead of a lock, consumers are idempotent because delivery is at-least-once, and reconciliation detects the divergence that escapes. There is no distributed lock in the design and no network call inside a database transaction. Read models close the loop: a projection is a function of the event log with a recorded position, so it can be rebuilt rather than patched, and where a user would notice the lag the response carries the position the write reached and the UI says “confirming” instead of showing a stale list. The rule in one sentence: strong consistency inside the boundary, well-governed eventual consistency across it, with observability good enough that divergence is found in minutes rather than at month-end.
| Scope | Guarantee | Mechanism | Cost accepted |
|---|---|---|---|
| One database, ordinary use case | atomic | one commit, no explicit transaction | none beyond the commit itself |
| One database, several aggregates | atomic | snapshot isolation in an explicit transaction | version-store growth, write conflict on commit |
| One database, invariant across rows | atomic, serialised | serializable plus a jittered retry policy | retries under contention |
| Concurrent edits | no lost update | row version, plus a unique constraint as the last line of defence | a conflict returned to the caller instead of a silent overwrite |
| Two or more services | convergent | outbox, saga with compensation, idempotent consumers, reconciliation | temporary divergence, made visible and repaired |
| Derived read model | eventual | projection with a position, rebuildable from the log | lag, surfaced to the client instead of hidden |
Live transaction-scope workflow proof
The running component below changes its scope/result view through real event binding. Select a scope to see the mechanism, the reasoning behind the choice, and the evidence that it holds.
Choose a consistency scope
Select a scope to inspect the mechanism, the reasoning and the evidence.
$ EF Core: DbContext + one SaveChangesAsync inside an explicit transaction · ADO.NET: BeginTransaction for multi-statement work
Inside one database a use case gets a real ACID boundary: the aggregate is loaded, changed and committed once. An explicit transaction is opened only where the work spans several statements or several aggregates, because opening one out of habit means holding locks for longer than the work actually needs.
use cases with exactly one commit: 100% transactions that wrap a network call: 0 SaveChanges calls per use case: 1 transactions opened just in case: 0
What choosing the scope deliberately buys
Source files
Select a file to inspect the proof with syntax highlighting and line numbers. The first file is the code running the component above; the others are the local transaction boundary with its isolation choices and retry policy, the rebuildable read model, and the map of which operation gets which guarantee.
Acceptance criteria
- Covers both scopes rather than choosing one and arguing for it.
- States the isolation level and its cost per use case instead of relying on the default.
- Shows an interactive proof of choosing a scope and reading the evidence.
- Names the guarantee and the detection method for every cross-service operation.
- Makes the read model rebuildable and surfaces its lag to the client instead of hiding it.