skip to content

Loading Strategies

How much of an object graph one read brings back and when: links that fault in on touch, eager plans, batched secondary statements, joins that multiply rows. Probed because it decides page latency.

on this pageshow

questions

29

What does it mean for a data-access layer to fill deferred links in identifier batches rather than one at a time?

level: juniorimportance: must knowfreq 52%

answer

  1. fewer statements, not zero statements
  2. one touch pays for many stand-ins
  3. keys gathered from the unit of work
  4. a set-membership predicate over identifiers
  5. N loads become N over batch size

basics

~20 s

Rather than one statement per touched stand-in, the layer gathers the keys of other unfilled stand-ins of the same kind and fetches them together with a set-membership predicate, turning N secondary statements into roughly N divided by the batch size.

solid answer

~40 s

A deferred link is filled on first touch, so a loop over many owners normally fires one small statement per owner. With identifier batching the layer, at the moment it has to fill one stand-in, looks at the other unfilled stand-ins of the same kind already registered in the current unit of work, takes up to a configured number of their keys, and fetches them all in one statement whose predicate is `... where id in (?, ?, ?, ...)`. Each returned row is matched back to its stand-in by key, so every link in that batch is now filled and costs nothing on the next touch. It does not remove the secondary statements; it divides their number by the batch size.

go deeper

for a junior

Recall the shape: one touch, one statement, many links filled. Be able to say that the statement uses a set-membership predicate over keys and that the count of statements drops rather than reaching zero.

for a middle

Explain where the keys come from — the unfilled stand-ins of the same kind in the current unit of work — and that rows are matched back by key, not by position. Name the over-fetch cost.

for a senior

Show when the trade turns bad: narrow children and a loop that touches everything is a clear win, wide children touched once is not. Reason about round trips versus rows transferred, out loud.

for a principal

Frame it as a per-link decision with a measurable payoff, not a global switch, and note that the size interacts with page sizes and with how many differently shaped statements the system emits.

## The situation batching is answering A **deferred link** is an association the data-access layer chose not to fill when it loaded the owning row. In its place it puts a **stand-in**: an object that carries the key and knows how to fetch the rest the first time anything touches it. That is what keeps the first read cheap and the object graph finite. The cost is deferred, not removed. A loop over fifty loaded owners that touches one deferred link each will fill fifty stand-ins, and if each fill is its own statement, that is fifty round trips to satisfy one screen. Batch fetching is the middle path between that and pulling the whole graph in the first statement. It keeps the loading deferred, and it keeps the extra statements — it just makes there be very few of them. ## What the layer actually does 1. Something touches a stand-in, so the layer must now fill it. 2. Before emitting anything, it inspects the other stand-ins **of the same kind** that are registered in the current unit of work and still unfilled. 3. It collects up to a configured maximum number of their keys — the **batch size** — including the key of the one that was touched. 4. It emits a single statement whose predicate is a set-membership test over those keys: `select ... from owner where id in (?, ?, ?, ?)`. 5. It routes each returned row to the stand-in that owns that key. Every stand-in in the batch is now filled. The touch that triggered the load pays for the whole batch. The next several touches in the loop find their objects already filled and emit nothing at all; only when the loop reaches a stand-in that was outside the batch does another statement fire. Fifty pending links at a batch size of ten produce five statements instead of fifty. ## Two things get batched, and they emit different statements | What is being filled | Statement the batch emits | What comes back | |---|---|---| | Stand-ins for single-valued links (the key sits on the owner row) | `select ... from target where id in (...)` | At most one row per key; a missing key means that link is genuinely absent | | Deferred collections (the key sits on the child rows) | `select ... from child where owner_id in (...)` | Many rows per key, filed under their owner; an owner with no children ends up with an empty collection | The collection case is the one that matters most in practice, because it is the case where the per-owner alternative is a statement per owner per screen. ## What it requires, and what it does not change - It needs the **keys already in hand**. Batching can only include stand-ins the unit of work already knows about; it cannot guess at links that have not been loaded or registered yet. - It needs the objects to be filled **in the same unit of work**. Once the owners are detached from the unit of work that loaded them, there is no shared registry to batch against. - It is **opportunistic, not a plan**. Nothing about the first query changes; the driving statement keeps the shape you wrote, with its own ordering and its own row count. - It can **over-fetch**. The batch fills links nobody has asked for yet. That is usually a win, because they are about to be asked for, and a waste when they are not. - The rows come back in whatever order the engine likes; the layer matches by key rather than by position, so the returned order carries no meaning. ## The partly filled last batch The pending keys rarely divide evenly by the batch size. The final statement of a run therefore covers fewer keys than the maximum, and layers differ in how they handle that: some simply emit a shorter list, some round down to one of a few pre-agreed list lengths, and some pad the list by repeating a key so that a small number of statement shapes recur. Which one a layer chooses is a detail of that layer, not a property of batching. ## How to talk about it in an interview Say what it is (fewer statements, not zero), say what triggers it (a touch, plus the other pending stand-ins of the same kind), say what it emits (one set-membership predicate over keys), and say what it costs (some links filled that were never needed, and a statement whose parameter list varies with how many keys were pending). That is the whole mechanism, and it is the same mechanism whichever data-access layer you have used.

  • If the batch fills links nobody ends up touching, has the batch made things worse?
    Sometimes. Batching trades wasted rows for saved round trips, and the trade is good whenever the extra rows are narrow and the links usually are touched. On a path that touches one owner's link out of a page of fifty, the batch fetches forty-nine sets of rows for nothing, and a smaller size or an explicit single load is the better call.
  • Why can a batch not be assembled for owners that were loaded in an earlier, already-finished unit of work?
    Batching works off the registry of stand-ins the current unit of work is holding. Objects that left that unit of work are detached: their unfilled links have no live registry to be gathered into a shared statement, and touching one is either an error or a fresh single load, depending on the layer.
  • Does batching change the driving query at all?
    No. The driving query executes exactly as written, with its own predicate, ordering and row count, and the batches are separate statements issued afterwards. That is precisely why batching is safe to switch on for a paged screen: the page is still the page you asked for.

It is the difference between walking to the storeroom once per item on the list and walking once with ten item numbers written down.

saying these in an interview costs you the question

  • Thinks batching removes the secondary statements instead of reducing their number
  • Believes the database, not the data-access layer, decides what goes in a batch
  • Assumes a larger batch size is always better
  • Thinks batching can fill links the unit of work has never registered
  • Confuses batching reads with sending many writes in one round trip
  • Expects batched rows to come back in the order the keys were listed
open as a page

When one statement fetches parents together with their child collection, why does the same parent come back repeatedly?

level: juniorimportance: must knowfreq 68%

basics

~20 s

The join returns one row per matching child, with the parent's columns repeated on every one of them. A layer that produces one result entry per row therefore hands back the same parent once per child row.

open as a page

When a data-access layer returns a deferred stand-in for a related object, which accesses fire a load and which do not?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Any access that needs data the row has not supplied fires the load: reading a mapped field, iterating or sizing a collection, or calling a behaviour method on the stand-in. Reading the identifier it already holds does not.

open as a page

What happens when code touches a not-yet-loaded association after the unit of work that loaded the object has closed?

level: juniorimportance: must knowfreq 74%

basics

~20 s

The read fails at that moment. A deferred link still needs its own query to fill itself, and the unit of work and connection that would run it are gone, so most layers raise an error instead of returning data.

open as a page

What does it mean to fill every owner's collection by re-running the original query's predicate as a subquery?

level: middleimportance: must knowfreq 46%

basics

~20 s

The layer issues one extra statement per deferred collection, reading children whose owner key comes from a subquery repeating the driving query's predicate. Statement count stops depending on how many owners returned, but that predicate is evaluated twice.

open as a page

How does declaring a link eager on the mapping differ from fetching it per query, and which choice can a query override?

level: middleimportance: must knowfreq 66%

basics

~20 s

A 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.

open as a page

Why does a lazily loaded stand-in break a type test, a cast, or an equality comparison against the mapped class?

level: middleimportance: must knowfreq 58%

basics

~20 s

A stand-in is an instance of a type the layer generated, not of the mapped class. Exact-class tests fail, casts to a concrete subtype fail even after loading, and equality over runtime classes is asymmetric.

open as a page

In a non-blocking data-access layer, why can a deferred association not fault in on access, and what replaces it?

level: middleimportance: must knowfreq 52%

basics

~20 s

A stand-in faults in by making the caller wait, and a non-blocking layer has nowhere to wait. So a plain accessor cannot load: links are named in the query, exposed as a deferred handle, or fetched by a second call.

open as a page

Which call sites commonly touch a deferred link after its unit of work has closed, and why do they escape review?

level: middleimportance: must knowfreq 62%

basics

~20 s

Four sites dominate: output serialisation, view rendering, async or queued work, and an object held past its request in a cache or field. All escape review because a deferred read looks like a plain field access, with no query at the call site.

open as a page

Why can a row limit not mean ten parents once the statement joins a child collection, and how do you page instead?

level: seniorimportance: must knowfreq 55%

basics

~20 s

A row limit counts grid rows, and a row is one parent-child pair, so ten rows may be two parents with a collection cut off mid-way. Page the parent keys first, then fetch those parents with their collection.

open as a page

Your endpoint fails on a deferred link touched after its unit of work closed - how do you rank the possible repairs?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Decide 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.

open as a page

When a data-access layer loads deferred links in identifier batches, how is each batch assembled and what happens to the final, partly filled batch?

level: middleimportance: should knowfreq 38%

basics

~20 s

The layer takes keys from unfilled stand-ins of the same kind in the current unit of work, up to a configured maximum, and emits one set-membership statement. Leftover keys form a shorter final list, sent as-is, rounded, or padded.

open as a page

How does a data-access layer remove the parents duplicated by a collection join, and why is a SQL DISTINCT no help?

level: middleimportance: should knowfreq 60%

basics

~20 s

De-duplication happens in memory, after the rows are assembled: entries sharing a parent key collapse to one. A SQL DISTINCT cannot do it because the rows differ in their child columns, so it removes nothing and adds a sort.

open as a page

What happens when one statement fetches a parent together with two of its child collections at once?

level: middleimportance: should knowfreq 57%

basics

~20 s

The two collections are crossed: each parent yields the product of the two child counts in rows, so four of one and three of the other give twelve rows. Each collection's contents are multiplied by the other's size.

open as a page

What is a named fetch plan in a data-access layer, and when is one better than naming the fetches inside each query?

level: middleimportance: should knowfreq 48%

basics

~20 s

A named fetch plan is a reusable, named description of which links a read should materialise, declared once and attached to a call. It beats inline fetches when several call sites share one shape, or when the read has no query text - a load by identifier.

open as a page

Why does a mapper replace the collection instance a constructor assigned with its own instrumented collection?

level: middleimportance: should knowfreq 44%

basics

~20 s

The layer needs a collection it controls: one recording whether the rows were read, which object owns it, and what was added or removed. On load it installs its own implementation, discarding the constructor's instance.

open as a page

When a non-blocking read hands back a stream of rows instead of a materialised list, what changes for the caller and the connection?

level: middleimportance: should knowfreq 44%

basics

~20 s

The call returns before any statement runs: work starts when the caller consumes, rows arrive one by one, and errors can surface mid-sequence. The connection stays checked out for the whole consumption, so a slow consumer holds it.

open as a page

Why might filling owner collections with a few extra batched statements beat one joined fetch, and what does each cost?

level: seniorimportance: should knowfreq 41%

basics

~20 s

A joined fetch buys one round trip and pays by repeating every owner column once per child row. Batched secondary statements send each row once, keeping bytes near owners plus children, and pay in extra round trips.

open as a page

In a paged parent list, why must the total-count statement not reuse the collection fetch joins?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Counting over a fanned-out grid counts parent-child pairs, so the total is inflated and the page count is wrong. Count parents: drop the collection joins, or count distinct parent keys when a filter forces the join to stay.

open as a page

An endpoint stays inside its statement budget yet is slow and heap-heavy under load - how would over-fetching explain that?

level: seniorimportance: should knowfreq 58%

basics

~20 s

Over-fetching costs bytes, not round trips: wide rows, whole graphs and unbounded collections pulled for a screen that shows a fraction. A statement counter stays flat while transfer, hydration and heap grow, so the budget never trips.

open as a page

How do you check whether a deferred link is already loaded before touching it, and when is that check the right tool?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Ask the layer, not the object: a load-state predicate reports whether a link or one attribute has been fetched, without touching it. Use it in generic code that walks object graphs and cannot know the caller's fetch plan.

open as a page

In a non-blocking service, how can a lost asynchronous context make statements run outside the intended transaction or on a second connection?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A blocking layer keeps the current unit of work in thread-bound storage. Non-blocking work resumes on other threads, so that slot is empty or holds another request's context, and the statement quietly runs on its own connection.

open as a page

A queued background job receives an object loaded by a finished web request and fails reading a deferred link - what went wrong?

level: seniorimportance: should knowfreq 48%

basics

~20 s

A mapped object was used as a message. Its unit of work ended with the request, so the deferred link has no loader and the filled fields are a stale snapshot. Send identifiers instead, and let the job read in its own unit of work.

open as a page

How would you choose the identifier-batch size for a service that fills deferred links in batches, and what would you measure to justify it?

level: principalimportance: should knowfreq 30%

basics

~20 s

Set it per access path, anchored to the page size that path returns, keeping the service to a few recurring sizes. Justify it with statements per request, rows fetched against rows touched, and tail latency before and after.

open as a page

As a lead, how would you bound what any single read may pull in a large mapped model where eager defaults have accumulated?

level: principalimportance: should knowfreq 40%

basics

~20 s

Push fetch decisions out of the mapping and into per-use-case plans, keep large values out of the rows that every read touches, cap what a collection fetch may return, and enforce a volume budget in tests - not only a statement count.

open as a page

How would you stop deferred-read-after-close failures from recurring across a whole codebase, and what does each policy cost?

level: principalimportance: should knowfreq 42%

basics

~20 s

Pick where the system pays: at design time, letting only fully materialised boundary types leave a transaction, or at runtime, keeping units of work open and absorbing implicit queries. The first costs mapping work; the second costs latency nobody sees.

open as a page

Your database work already runs behind a bounded connection pool, so what does a non-blocking data-access layer buy, and when is keeping the blocking one better?

level: principalimportance: nice to knowfreq 34%

basics

~20 s

The pool bounds database concurrency either way, so a non-blocking layer buys no extra throughput. It buys threads: waiting requests stop occupying them, so overload queues instead of exhausting a thread pool. It costs explicit fetching and manual context.

open as a page