skip to content

Demarcation & Propagation

Opening one unit of work per use case, declaring the boundary or coding it by hand, and how a nested call joins, suspends or refuses. Asked because misplaced boundaries leave half-written data.

on this pageshow

questions

6

Why is a transaction boundary usually placed around one use case rather than around each repository call?

level: juniorimportance: must knowfreq 72%

answer

  1. atomicity has a scope
  2. the boundary decides what rolls back
  3. one use case, one commit
  4. repositories enlist, they do not commit
  5. no wider than the use case

basics

~20 s

A transaction commits or rolls back as a whole, so the boundary must enclose everything that has to be all-or-nothing - usually one use case. Per-call commits make each write permanent on its own, so a later failure leaves half-written data.

solid answer

~40 s

The boundary defines atomicity: whatever runs inside it either lands together or not at all. A use case - place an order, transfer funds, close an account - is normally that unit, because it is what the business treats as one change. If each repository call opens and commits its own transaction, the order row is already permanent when the stock update fails, and there is no rollback left to undo it; you end up writing compensation by hand. So demarcation belongs one layer above persistence, in the service or handler that owns the use case, while repositories stay boundary-agnostic and simply enlist in whatever is already open. The boundary should also be no wider than the use case: stretching it over a whole request holds locks during work that is not database work.

go deeper

for a junior

Remember that a transaction is what commits or rolls back together, and that the code decides how much is inside it. Name the use case as the usual unit and the service layer as where it is opened.

for a middle

Explain what per-call commits actually leave behind after a mid-way failure, and why a repository must not commit on its own. Mention that a boundary wider than the use case costs lock time.

for a senior

Show how you spot a misplaced boundary from production evidence - partial data, hand-written compensation, lock waits that track request latency - and how you move it without breaking callers.

for a principal

Frame it as a policy: one predictable default boundary per use case, with any deviation reviewed and its recovery path written down, so nobody invents per-case rules under deadline pressure.

## What a transaction boundary is A **transaction** is the smallest unit a database will make permanent as a whole. Everything issued between the moment it starts and the moment it commits shares one fate: all of it lands, or none of it does. **Demarcation** is the act of deciding where that start and that commit sit in your code. That makes demarcation a design decision, not a technical detail. It fixes exactly which set of writes the system promises to be all-or-nothing, and therefore which half-finished states a reader or a crash can ever expose. If nothing demarcates anything, a connection typically runs in **autocommit**: every statement is its own transaction and becomes durable the instant it succeeds. That is still a boundary - just the smallest one available, chosen by default rather than by you. ## Why the use case is the natural unit A **use case** is one business-meaningful change: accept an order, transfer funds, cancel a subscription. It is the right default unit for four reasons. - **Atomicity matches the rule.** The invariant the business states ("an order always has its stock reserved") is a statement about the use case, so the guarantee has to have the same shape. - **Rollback is the only cheap undo.** Inside one boundary, failure costs nothing but an abort. Across boundaries you must write compensating writes by hand, and those can fail too. - **Readers see one step.** Other transactions observe the change as a single event rather than catching it mid-way (the exact visibility rules are an isolation-level question, owned elsewhere). - **Retry has a unit.** A whole use case can be replayed after a transient failure; half a use case cannot. ## Granularity compared | Boundary | Atomic unit | Typical problem | |---|---|---| | Per statement (autocommit) | one statement | any multi-statement rule can break mid-way | | Per repository call | one repository method | half-written use cases with no undo left | | Per use case | the business change | the default worth defending | | Per request | everything the request touches | locks held during work that is not database work | The two rows either side of "per use case" are the common mistakes. The first is usually accidental - nobody opened a boundary. The last is usually deliberate and defended as convenience, and it hurts under load rather than in tests. ## Where the boundary is declared Put it in the layer that names the use case: the service method, command handler or interactor. Repositories should be **boundary-agnostic** - they issue statements on whatever transaction is currently in effect and never commit on their own. This has a practical payoff: 1. Two repository calls compose into one atomic change without either repository knowing about the other. 2. The same repository is reusable from a use case with a different boundary shape. 3. Tests can wrap a whole scenario in a boundary and discard it afterwards. A repository that opens and commits its own transaction destroys all three, because a caller can no longer group its calls. ## Symptoms of a misplaced boundary - Data that "sometimes" shows a parent row with no children, or a total that disagrees with its parts. - Cleanup code that deletes rows a previous step already committed - compensation written because rollback was unavailable. - Errors reporting failure to the caller while the change is partly visible in the database. - On the too-wide side: lock waits and timeouts that appear only under concurrency, and transactions whose duration tracks request latency rather than the work they do. ## What the boundary does not decide Demarcation answers *where the unit starts and ends*. It does not by itself answer how long the layer's tracked set of loaded objects stays usable, when connections are acquired and released, what concurrency anomalies remain visible at a given isolation level, or how a conflict is detected. Those are separate questions with their own answers; conflating them is why "just make the transaction longer" is such a common bad fix. ## The rule of thumb Start every write path with one boundary per use case, opened in the service layer. Narrow it only when a sub-operation genuinely has a life of its own, and widen it never - if two use cases must be atomic together, that is a sign they are really one use case and should be named as such.

  • If two use cases must succeed or fail together, should you wrap both in one boundary?
    Occasionally, but treat it as a smell. If they are genuinely atomic, they are one use case and should be named as one, with a single service method owning the boundary. Wrapping two independently-invocable use cases in an outer boundary quietly changes the failure semantics of both and makes each one's contract depend on who called it.
  • How do you keep repositories boundary-agnostic in practice?
    Give them no commit, rollback or boundary-opening calls at all - they only issue reads and writes on whatever transaction is currently in effect. The caller owns the boundary. A quick audit is to grep the persistence layer for commit and rollback: any hit there is a repository making a decision that belongs to the use case.

saying these in an interview costs you the question

  • Thinks separate per-call commits still give the use case all-or-nothing behaviour
  • Opens and commits a transaction inside the repository, so callers cannot group calls
  • Believes a rollback can undo writes an earlier, already-committed call made
  • Widens the boundary to the whole request because it is convenient
  • Cannot say what the boundary is for beyond 'the framework wants one'
open as a page

When an operation with its own declared boundary is called inside an existing transaction, what propagation choices exist?

level: middleimportance: must knowfreq 70%

basics

~20 s

Propagation decides what an inner bounded call does when a transaction is already running: join it, suspend it for an independent one, refuse to run unbounded, or open a nested scope that can be discarded on its own.

open as a page

When a transaction boundary is applied by a wrapper around an object, why can a call between its own methods run untransacted?

level: middleimportance: must knowfreq 66%

basics

~20 s

Interception happens as a call passes through the wrapper. A method calling another on the same object uses the object's own reference, so the wrapper is never crossed, no boundary begins, and the inner method's declared attributes are silently ignored.

open as a page

In a data-access layer, how does declarative transaction demarcation differ from opening the boundary programmatically?

level: middleimportance: should knowfreq 60%

basics

~20 s

Declarative demarcation attaches the boundary to a method as metadata and lets a wrapper begin, commit or roll back around the call. Programmatic demarcation writes those steps in the code, buying exact scope and runtime-chosen attributes at the cost of boilerplate.

open as a page

Why declare a timeout on a transaction boundary, and what happens to the work when that timeout expires?

level: seniorimportance: should knowfreq 48%

basics

~20 s

A timeout caps how long one unit may hold locks and a connection, so one stuck statement cannot pile callers up behind it. When the deadline passes the unit is failed and rolled back in full - nothing partial survives.

open as a page

How do you decide whether a use case with several independent sub-operations runs under one boundary or one per sub-operation?

level: principalimportance: should knowfreq 44%

basics

~20 s

Start from the invariant: only a state no reader may ever observe justifies one boundary. Otherwise weigh a long unit's lock time against the obligation a split creates - an intermediate state that must be detected, resumed and safely repeated.

open as a page