How do per-transaction, per-request and conversation-spanning unit-of-work scopes differ, and what does each one cost?
answer
- two clocks: objects and connection
- shorter scope, less staleness
- widening the scope widens connection hold time
- conversation keeps objects, not transactions
- detached state plus version check
basics
~20 sThe three scopes differ in how long the tracked set and its connection stay alive - one transaction, one request, or a flow spanning requests. Longer scopes reuse more objects, go stale more often, and hold a shared connection longer.
solid answer
~40 s**Per-transaction** is the tightest: the unit of work is created at the boundary, does its statements, commits and is discarded, so the connection is out of the pool for milliseconds and nothing survives to go stale. **Per-request** keeps one unit for the whole handler, so repeated reads of a row reuse one object and one transaction covers several service calls - paid for by holding the connection through everything the handler does, including work that touches no database. **Conversation-spanning** keeps the tracked set alive across the steps of a flow, but must not keep a connection or transaction open across user think-time, so in practice it is detached state plus a short transaction per step. Scope length should track how long you need statements, not how long you need objects.
go deeper
Learn the three names and the one sentence that separates them: how long the loaded objects and the connection stay alive — one transaction, one request, or a flow spanning several requests.
Explain the trade the widening makes: more object reuse and one commit, paid for with a connection held through everything else the handler does, plus a longer window for data to go stale.
Show you would keep the connection short even when the objects live long — detached state and a version check for conversations — and connect a starved pool back to whichever scope grew.
Treat scope as a policy per workload class rather than one global default, and make open time a budget the team reviews, since it is what determines how much concurrency a fixed pool can serve.
## The two clocks Every discussion of scope confuses two clocks unless you separate them: - how long the **tracked set** lives — the loaded objects, their pending changes and the identity guarantees over them; - how long the **connection and transaction** live — a slot in a shared pool and open state on the server. The first is a per-process memory question. The second is a service-wide capacity question. Short scopes keep them together and that is why they are easy to reason about; every longer scope is an attempt to keep objects alive *without* keeping a connection alive for the same span. ## Per-transaction scope The unit of work begins at a transactional boundary, issues its statements, commits, and is thrown away. Objects it loaded become detached the moment it ends. - **Cost:** the connection is held only for the statements themselves. - **Trade-off:** nothing is reusable across boundaries, so two boundaries reading the same row read it twice and get two objects. - **Fits:** anything called from a queue consumer, a job step, or a service method that is complete in itself. ## Per-request scope One unit of work is created when the request arrives and disposed when the response is finished, usually with one transaction inside it. It is the default in most server applications because it makes the request the natural consistency boundary. - Reads of the same row anywhere in the request return one object, so layers do not fight over stale copies. - All the writes commit or roll back together without every method declaring its own boundary. - **The cost is the connection.** It is held for everything the handler does inside the boundary — validating input, calling another service, formatting, serialising — not only for the statements. A handler that spends 30 ms querying and 300 ms elsewhere occupies a pool slot for 330 ms. ## Conversation scope Some flows genuinely span requests: a multi-step wizard, an edit screen with a preview, a long form where the user's intermediate edits must survive until a final confirmation. A conversational unit of work keeps the tracked set alive across those requests so the accumulated edits flush once at the end. The thing to be firm about: **the tracked set may span requests; the connection and the transaction must not.** Holding a transaction open across human think-time hands an unbounded pause to a shared resource. So a workable conversation keeps the state as detached objects (in the client, or in a store keyed by the flow), and each step opens a short unit of work of its own, with a version column carrying the concurrency check that the long transaction would otherwise have provided. ## Comparison | | Per-transaction | Per-request | Conversation | |---|---|---|---| | Tracked set lives | One boundary | One request | Many requests | | Connection held | Statements only | Whole handler body | Per step only, never across think-time | | Object reuse | None across boundaries | Within the request | Across the whole flow | | Staleness risk | Lowest | Bounded by request length | High — data can move under the user | | Memory growth | Negligible | Bounded by one request | Needs an explicit end and an expiry | | Typical failure | Repeated reads of the same row | Pool starved by slow non-database work | Abandoned flows that are never cleaned up | ## Choosing, and the failure signature to watch A usable ordering when you are deciding: 1. Start per-transaction, and let the boundary be the smallest piece of work that must be atomic. 2. Widen to per-request when the handler genuinely needs one consistent view and one commit. 3. Reach for a conversation only when the *user's* work spans requests, and implement it with detached state plus a version check rather than with a held transaction. Whatever you pick, the number that matters operationally is **open time per unit of work, not statements per unit of work.** Pool starvation is caused by units that are open while the process is doing something else, so a long scope is safe exactly to the degree that it is not holding a connection. When callers start timing out on borrowing while the server is idle, the first question is which scope grew, not how many queries ran.
- Why is a transaction held across user think-time considered unacceptable rather than merely slow?Its duration is set by a human, so it is unbounded. It occupies a shared connection slot and keeps server-side transaction state alive for as long as someone leaves a tab open, and any locks it took block others for the same span. Nothing in the system can plan capacity against a number a user chooses.
- If per-request scope holds the connection through non-database work, why is it still the common default?Because for a typical handler the non-database work is short and the simplicity is worth it: one consistent view, one commit, no per-method boundary declarations. It stops being a good default the moment a handler does something slow inside the boundary — a call to another service, a large computation, or streaming a response.
- How do you keep a conversation from accumulating abandoned state?Give the flow an explicit end on both paths — completion and cancellation — plus an expiry so abandoned flows are discarded without anyone acting. Store the state where an expiry is natural rather than in process memory, and size it, since an unbounded store of half-finished edits is the same leak in a different place.
A per-transaction scope is borrowing a tool, using it and returning it; a per-request scope is keeping it on your desk all shift; a conversation that holds a connection is taking it home over the weekend while nobody else can work.
saying these in an interview costs you the question
- Thinks a longer scope is simply better because fewer objects reload
- Keeps a transaction open across user think-time in a wizard flow
- Assumes per-request scope holds the connection only during queries
- Believes a conversation needs one long transaction to stay consistent
- Judges scope by statement count rather than by how long it stays open