RTS questionnaire · Q36 · Transaction scope proof

Have you worked with both transactions within a single application/database and transactions or consistency requirements spanning multiple services/databases?

Both scopes and, more importantly, the line between them: the isolation level chosen per use case with its cost, the guards that actually hold under concurrency, and the read model that makes eventual consistency honest in the UI.

Status: reviewed Live transaction-scope workflow ACID · Isolation levels · Outbox · Read models Jump to live proof ↓

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.”
Technical decision: expand. The submitted line claims both scopes; the interesting part is the rule that decides between them and the cost of the local side, which is the half usually left unsaid. This page is the separate, detailed version: the isolation level chosen per use case with the reason and the cost, the guards that survive real concurrency, the cross-service mechanisms with a pointer to their detail, and the read model that makes the eventual half trustworthy.

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.

ScopeGuaranteeMechanismCost accepted
One database, ordinary use caseatomicone commit, no explicit transactionnone beyond the commit itself
One database, several aggregatesatomicsnapshot isolation in an explicit transactionversion-store growth, write conflict on commit
One database, invariant across rowsatomic, serialisedserializable plus a jittered retry policyretries under contention
Concurrent editsno lost updaterow version, plus a unique constraint as the last line of defencea conflict returned to the caller instead of a silent overwrite
Two or more servicesconvergentoutbox, saga with compensation, idempotent consumers, reconciliationtemporary divergence, made visible and repaired
Derived read modeleventualprojection with a position, rebuildable from the loglag, 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.

LIVE PREVIEW
RUNNING
TRANSACTION SCOPE WORKFLOWRUNNING

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

Rules I apply. Choose the isolation level per use case and write down its cost, because “the default is fine” is a decision made by omission; keep the transaction short, since lock duration is the only thing that makes two correct transactions contend; never hold a lock across a network call, which is the mistake that turns a slow dependency into a database incident; let a constraint be the last line of defence even when the code already checks, because it is the only check that survives two concurrent requests arriving at the same instant; state the guarantee for every cross-service operation and name how a divergence would be detected, since an undetectable divergence is not consistency but hope; and make derived data rebuildable, because data that can only be patched is data nobody dares to fix.
Boundary for Q36: this page compares the two scopes and draws the line between them. The distributed mechanisms themselves — saga state, compensation, the outbox relay, the reconciliation job — are on the Q35 proof page; the resilience machinery around them is on Q34; and the unit-of-work read of the local side is on Q32.

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