Your endpoint fails on a deferred link touched after its unit of work closed - how do you rank the possible repairs?
answer
- who decides what is loaded
- shape belongs at the read
- project, then name links
- keeping it open hides the cost
basics
~20 sDecide the shape at the read: project to a transfer model for read-only output, or name the links the use case needs when loading the root. Below those, initialise explicitly before the object leaves. Keeping the unit of work open ranks last.
solid answer
~50 sRank by how explicit the resulting data shape is. **First**, if the caller only renders a few fields, query a narrow transfer model - nothing deferred leaves the transaction, and the query says exactly what the screen needs. **Second**, if the caller genuinely needs the graph, name those links in the read that loads the root, so one planned statement replaces an unplanned one. **Third**, touch the links explicitly before the object leaves - it works, but it is easy to drift out of step with the caller and often costs one statement per link. **Last**, keep the unit of work open across output: it makes the symptom disappear while leaving the output format in charge of which queries run and holding resources for the whole span. Making everything eager is not on the list - it fixes this path by slowing every other one.
code
pseudocode · 11 lines// fails: the link is touched after the unit of work ends
order = withUnitOfWork { load(Order, id) }
render(order.lines) // needs a query here; nothing can run it
// repair 2: name what this use case needs, inside the read
order = withUnitOfWork { load(Order, id, fetch = [lines]) }
render(order.lines) // already materialised
// repair 1: for a read-only screen, never let a deferred link out
rows = withUnitOfWork { query(Order).where(id = ?).select(id, total, lineCount) }
render(rows)go deeper
Learn the order: say what you need when you read, rather than hoping the data is still reachable later. A projection for a screen, a named link for a graph, and keeping things open only as a last resort.
Explain why explicit initialisation before leaving is weaker than naming links in the read: it drifts from what callers need and tends to cost one statement per link instead of one planned statement.
Show that you verify rather than assume - re-run the path with the unit of work closed before output, compare statement counts before and after, and reject a repair that trades an error for per-row reads.
Position it as a policy: the shape of a read is a design decision that belongs beside the query. Anything that lets output code decide it - open contexts, eager mappings - moves cost into the least reviewable place in the system.
## The principle behind the ranking Every repair answers the same question - *who decides what is loaded?* - and they differ only in whether the answer is written down. The best repairs move the decision **into the read**, where it is visible, reviewable and bounded. The worst ones leave it with whatever code happens to touch the object later. ## The ranking 1. **Project to a transfer model in the query.** For a read-only path, select the fields the caller renders and return a shape that has no deferred links at all. The failure becomes impossible rather than avoided, the payload is bounded, and the query documents the screen. 2. **Name the required links in the read.** When the caller needs the actual graph - because it applies domain behaviour, not just formatting - specify those links when loading the root. One planned statement, or a small planned set, replaces an unplanned one; the shape lives next to the query. 3. **Initialise before the object leaves.** Touch the links inside the transaction so they are filled by the time it closes. Correct, but weak: the list of touches drifts as callers change, and it usually costs one statement per link rather than one planned read. 4. **Keep the unit of work open across output.** The symptom disappears because the loader is still alive. Nothing else improves. ## What each option costs | repair | fits when | costs | |---|---|---| | project to a transfer model | read-only output, list and detail screens | a second shape to maintain; not usable where domain behaviour is needed | | name links in the read | the caller needs real objects and their graph | one plan per use case; plans multiply if not curated | | initialise before leaving | a quick, local repair on one path | drifts from caller needs; often one statement per link | | keep the unit of work open | a legacy path you cannot restructure now | resources held for the whole span, query count set by output, cost invisible in review | ## Why the last option ranks last It is not that it fails. It is that it **converts a loud failure into a quiet cost**. With the loader alive through output, whatever the renderer reaches for is fetched, so the output format silently decides how many statements run and how much of the graph is materialised. Two things get worse together: the queries happen at the point where the least is known about their consequences, and the resources stay busy across the whole rendering span. It also removes the exact signal - the failure - that told you the read did not match the use case. ## Why turning associations eager is not a repair Making links load with their parent does stop the failure, but it applies the change everywhere the parent is read, including paths that never wanted the extra data. The result is a repair whose cost is paid by unrelated code, and it cannot be reasoned about per use case, which is exactly what the top two repairs give you. ## Choosing between the top two Ask what the caller does with the object: - **Renders it** - project. If the only consumer is a screen or a payload, a transfer model matches it exactly and cannot regress into a deferred read later. - **Decides with it** - name the links. Domain behaviour needs real objects with real invariants; a flattened shape would just be reassembled badly. - **Both, on different paths** - use both, and keep the read path free of mapped objects entirely. ## Verifying the repair, not assuming it A repair is done when three things hold: 1. **The failing path no longer touches anything unfilled.** Re-run it with the unit of work closed before the output step, so an outstanding stand-in still fails loudly. 2. **The statement count did not grow.** If a repair replaced one failure with a per-row read, latency has been traded for correctness rather than earned. 3. **The shape is stated at the read.** Someone reading the query can tell what the response will contain without opening the output code. If all three hold, the decision has moved into the read - and that, not the absence of the error, is what makes the class of failure stop recurring.
- When is keeping the unit of work open genuinely the right call?As a deliberate, temporary measure on a path you cannot restructure yet - a legacy view with many touch sites - and only with the cost acknowledged: statements driven by the renderer and resources held across output. Treat it as debt with an owner, not as the house style.
- The repair fixed the error but the endpoint got slower. What likely happened?Explicit initialisation before leaving was probably used, filling each link with its own statement per parent row. That turns one failure into many round trips. Replace it with a single read that names the links, or with a projection, and re-check the statement count.
- How do you stop named fetch plans from multiplying out of control?Tie each plan to a named use case rather than to an entity, review them as part of the endpoint they serve, and delete them with it. Where several use cases want the same narrow data, a projection is usually the cheaper answer than another graph plan.
saying these in an interview costs you the question
- Reaches first for keeping the unit of work open through output
- Marks associations eager in the mapping to fix one endpoint
- Touches links in a loop before returning and calls it fixed
- Catches the failure and returns an empty link instead
- Never checks whether the statement count grew after the fix