How does declaring a link eager on the mapping differ from fetching it per query, and which choice can a query override?
answer
- blast radius, not syntax
- one is global, one is local
- widening works, narrowing usually does not
- loads by identifier inherit the mapping
- prefer the failure that is visible
basics
~20 sA mapping-level eager declaration is global: every read of the owner pays it, including loads the layer issues itself. A per-query fetch is local to one use case. Queries can usually widen a deferred link, but rarely narrow an eager one.
solid answer
~50 sThe two differ in **blast radius**. A fetch type written on the mapping becomes the link's default for every read of the owner - hand-written queries, loads by identifier, links reached through another object's plan - so one line commits every present and future caller. A fetch named inside a query applies to that query alone, so each use case declares what it needs. The asymmetry that matters in practice is **overriding**: naming a link in a query reliably *widens* what a read materialises, because the layer simply adds it to the plan. Going the other way - suppressing a link the mapping declared eager - is not reliably available, because the layer treats the mapping fetch type as part of what the object *is*. That is why the workable default is to defer in the mapping and widen deliberately per use case: the local decision can always be added, while the global one is hard to take back at the call site.
go deeper
Remember the two places a fetch can be asked for - the mapping and one query - and that the mapping one applies to every read of that object, not just the query in front of you.
Explain the asymmetry: a query can add a link to what a read materialises, but usually cannot remove one the mapping declared. Then explain why that makes deferring the safer default.
Talk about discovery. Over-fetch from a mapping default surfaces as latency and heap far from the change; a missing fetch surfaces immediately. Choose the failure mode you can find.
This is a policy about where a cost decision may be made. Global defaults are inherited by code that does not exist yet; per-use-case fetches keep cost next to the caller who pays it.
## Two declaration sites, two blast radii A mapping layer accepts fetch instructions in two places, and the difference between them is not stylistic. | | Mapping default | Per-query fetch | |---|---|---| | Scope | every read that returns the owner | one query | | Applies to loads by identifier | yes | no - there is no query text to attach it to | | Applies to reads written later | yes, automatically | only if that query asks | | Reviewed by | whoever edits the mapping | whoever writes the query | | Cost visible at | the mapping file, far from the endpoint | the call site | The mapping default is the one that behaves like a global variable. It is inherited by reads that did not exist when it was written, by reads that never touch the link, and by internal loads the layer performs to satisfy other work. That is its whole point when the link genuinely is needed everywhere - and its whole problem when it is not. ## The overriding asymmetry The practical rule is that **widening is easy and narrowing is not**. - **Widening.** A query can name a deferred link and ask for it to be fetched with the result. The layer adds it to the read's plan; the mapping is untouched, and other callers are unaffected. This direction is well supported. - **Narrowing.** A query generally cannot say "ignore the eager declaration and leave this link empty." The layer regards a mapping-level eager link as part of the object's definition, so it materialises it anyway. Some layers offer a partial escape - reading a narrower shape than the mapped object at all - but that is a different construct, not an override of the fetch type. Because of that asymmetry, the two declaration sites are not symmetric knobs. One is a floor that every read stands on; the other is an addition each read may make. ## What a mapping-level eager default actually commits you to 1. **Every read of the owner, forever.** A list screen that shows a name loads the graph the mapping named. A count-and-check code path loads it too. 2. **Transitivity.** If the eagerly linked object itself declares eager links, the read follows them. Two or three innocent hops can pull an unexpectedly large slice of the graph. 3. **Statement shape you did not choose.** The layer may join, or may fire immediate secondary statements; either way the shape is decided by the mapping, not by the use case. 4. **A cost that is hard to attribute.** The slow endpoint's query looks innocent; the expensive instruction lives in a file the endpoint's author never opened. ## What a per-query fetch commits you to 1. **Saying it every time.** Each use case that needs the link must name it, which is repetition - the reason named, reusable plans exist. 2. **Failing loudly when you forget.** A link nobody fetched stays deferred, and touching it later either loads it then or fails outright if no unit of work is open. That is a visible, fixable error, unlike silent over-fetch. 3. **Local reasoning.** The read's cost is written where the read is. ## Choosing a direction of error Both arrangements can go wrong; they fail differently. | Default | Typical failure | How it is discovered | |---|---|---| | Eager in the mapping | silent over-fetch: bytes, hydration, heap on every read | a latency or memory investigation, often much later | | Deferred in the mapping | a link that was needed but never fetched | an error or an extra statement, usually on first exercise | A missing link announces itself; an over-broad default does not. That asymmetry, more than any absolute rule, is why teams tend to defer by default and widen per use case, and why a proposal to flip a link to eager "so nothing breaks later" deserves the question: which reads of this owner will now carry it, and do all of them need it? ## How to check what a read is actually doing - Log the statements one endpoint emits and read the column list, not just the count. - Count the objects materialised versus the number the response uses. - When a read looks heavier than its query text, look at the mapping before blaming the query - the instruction is often not where you are reading. - After changing a mapping default, re-check the *other* endpoints that return the same owner; they inherited the change without being edited.
- Why can a query not simply switch off a link the mapping declares eager?Because the layer treats the mapping fetch type as part of what a fully materialised object is, and it returns fully materialised objects. Leaving the link empty would hand back something that looks like the mapped object but is not. Reading a narrower shape instead of the mapped object is the usual escape, and that is a different construct rather than an override.
- How does a mapping-level eager link affect reads nobody wrote as a query?It applies to them too. A load by identifier, a link traversed from another object, or an internal load the layer performs all return the owner, so all of them pull the eager link. That is why statement logs sometimes show reads with no matching query in the codebase.
- If eager defaults are risky, why do layers offer them at all?Because some links genuinely are part of the object for every use case - a small, bounded reference that every screen displays, or a value object stored in its own table. Declaring that once is clearer than repeating it in every query, and it removes a whole class of forgotten-fetch errors for that link.
saying these in an interview costs you the question
- Treats the mapping default and a per-query fetch as interchangeable knobs.
- Believes any query can suppress an eager link declared on the mapping.
- Thinks a mapping default applies only to queries the team wrote.
- Argues eager everywhere is safe because nothing can fail later.
- Ignores that eager links can chain transitively through the graph.