skip to content

Repository Boundary

Where a query lives — one interface per aggregate, one per table, or inline in a service — and whether it returns a tracked object or a transfer model. Interviewers test what the boundary hides.

on this pageshow

questions

4

In a data-access layer with a unit of work, how does returning a tracked object differ from returning a transfer model?

level: juniorimportance: must knowfreq 64%

answer

  1. who is still watching the object
  2. snapshot versus live registration
  3. edit equals write, at flush
  4. safe to hold past the transaction?

basics

~20 s

A tracked object stays registered with the open unit of work, so later edits to it are written at flush and unloaded parts can still be fetched. A transfer model is a plain snapshot that does neither.

solid answer

~50 s

A **tracked object** is one the layer is still watching inside its unit of work. It sits in the identity map, it is compared against the snapshot it was loaded with, so assigning a field is enough to produce an `UPDATE` at flush, and parts the mapping left unloaded can usually still be fetched while the scope is open. All of that is a property of the open scope, not of the object. A **transfer model** is a plain object built from selected values: the layer does not track it, editing it writes nothing, nothing arrives late, and it is safe to cache, serialise or hand to code with no transaction. So the return type decides how far the transaction really reaches — a boundary that hands out tracked objects has published its unit of work to every caller above it.

go deeper

for a junior

Recall the two shapes and the one-line difference: a tracked object is still being watched by the open scope, a transfer model is a snapshot. Then say which one is safe to hold after the transaction ends.

for a middle

Explain the mechanics: identity map, snapshot comparison at flush, and why no save call is needed. Say what the caller gives up with a snapshot - no write-back, no late fetching, no domain rules.

for a senior

Show that you treat the return type as a contract about transaction scope. Point at the concrete failures: an edit outside a scope that silently vanishes, and an edit inside one that silently persists.

for a principal

Frame it as policy. Which call sites are allowed to hold tracked objects at all, how that rule is expressed so it survives new code, and what it costs to standardise on snapshots across a large codebase.

## The choice the boundary makes Every data-access method has to hand something back, and there are two fundamentally different things it can be. Two terms first, because everything below depends on them. A **unit of work** is the scope in which a data-access layer keeps the objects it has loaded, records what changed, and writes those changes out in one go when it flushes and commits. An object the layer loaded and kept in that scope is a **tracked object**: the layer holds it in an identity map (one instance per row key inside the scope) and compares it against the snapshot it was loaded with, so a plain field assignment is enough to produce an `UPDATE` at flush. That comparison is usually called **dirty checking**. A **transfer model** is a plain object built from selected values. The layer does not keep it, does not compare it against anything, and does not know it still exists after the call returns. ## What comes with a tracked object - **Identity.** Two calls that reach the same row inside the same scope hand back the same instance, so the caller cannot end up holding two disagreeing copies. - **Mutation means write intent.** Assigning a field is the save. There is usually no explicit save call for an object that is already tracked; the write happens when the scope flushes. - **Parts can arrive late.** Anything the mapping left unloaded can typically still be materialised while the scope is open, which is exactly why the object is convenient and exactly why it is fragile once the scope closes. - **Mapped behaviour and invariants.** The object is the mapped type, so whatever rules and methods the domain type carries come with it. - **A binding to the scope.** The write-back and late-loading guarantees are properties of the open scope, not of the object. What the object becomes after the scope closes is its own subject. ## What comes with a transfer model - **No strings.** It can be cached, queued, serialised, handed to another thread, returned to a caller that has no transaction at all. - **No accidental writes.** Editing it is a local edit and nothing more, which is a feature on read paths where nobody intends to write. - **A shape chosen per use case.** It can flatten several graphs, carry a computed count, or drop most columns, because nothing forces it to mirror one mapped type. - **No invariants and no way home.** It carries values, not rules, and updating the underlying rows needs a deliberate load-and-modify inside a scope or a set-based statement. ## Side by side | What the caller asks | Tracked object | Transfer model | |---|---|---| | Do my later edits get written? | Yes, at flush, while the scope is open | No | | Can I hold it past the transaction? | Not for the guarantees above | Yes | | Can unloaded parts arrive later? | Typically, while the scope is open | No | | Who chooses the columns? | The mapping | The call site | | Does it carry domain rules? | Yes | No | ## Why the return type is really a transaction decision A boundary that hands out tracked objects has published its unit of work to everything above it. Code that reads such an object is inside a transaction whether or not it says so, and code that edits one either writes to the database without a save call or loses the edit entirely, depending on whether a scope is still open around it. Both outcomes are silent. That is the whole reason interviewers ask: the return type, not the method name, decides how far the transaction reaches. Data-access layers differ here, and the difference is worth stating plainly: some track everything they return by default and need an explicit opt-out to produce read-only results, while others have no tracked set at all and can only ever produce snapshots, so for them the question does not arise and every write is explicit. A design that assumes one of these behaviours will misbehave on the other. ## Making the choice visible at the boundary 1. **Say it in the signature.** A method that returns a mapped type is offering a tracked object; a method that returns a purpose-built model is offering a snapshot. Do not mix both meanings under similar names. 2. **Default read paths to snapshots.** Anything whose result crosses a transaction boundary, gets serialised, cached or queued should be a transfer model, because none of the tracking guarantees survive that trip anyway. 3. **Keep writes inside a scope-owning method.** Load, mutate and let the scope flush in one place, rather than returning a tracked object and hoping the caller mutates it in the right context. 4. **Treat a tracked object reaching a rendering or serialisation layer as a defect**, not as a convenience. It is the shape that produces both silent writes and late failures. The choice is not permanent, and it is not global. It is made per method, and the honest question at each one is: does this caller intend to write, and is it inside the scope that would carry the write?

  • If a boundary returns tracked objects, what has it implicitly told its callers about transactions?
    That they are running inside one, or should be. The write-back and late-fetch guarantees only hold while the scope is open, so every caller now has an unwritten precondition. Callers that honour it write to the database with no save call; callers that do not lose their edits silently. Neither shows up in the signature.
  • Does returning a transfer model mean the caller can never update the underlying data?
    No - it means the update has to be deliberate. The caller sends the change back through a write method that loads and mutates inside a scope, or the layer issues a set-based statement. What disappears is the accidental path where an edit made anywhere becomes a write.
  • Why can two calls inside one scope not hand back two different instances for the same row?
    Because the layer keys tracked objects by identity inside the scope. The second load finds the instance already there and returns it rather than building a second one, which keeps the caller from holding two copies that disagree and from producing conflicting writes at flush.

A tracked object is a library book still checked out in your name: whatever you write in it goes back with the book. A transfer model is a photocopy - yours to mark up, and the shelf copy never changes.

saying these in an interview costs you the question

  • Thinks a tracked object needs an explicit save call before any change is written
  • Believes each field assignment on a tracked object issues its own statement
  • Says a transfer model can be edited and committed like a tracked object
  • Assumes the tracking guarantees still hold after the scope closes
  • Treats the return type as a style preference rather than a transaction decision
  • Expects unloaded parts of a transfer model to be fetched on first access
open as a page

A data-access interface hides how its results are fetched. Which fetching decisions can it hide from callers, and which cannot?

level: seniorimportance: must knowfreq 58%

basics

~20 s

It can hide the statement text, join strategy, chosen columns and index or cache use. It cannot hide cost or timing: statement count, what was left unloaded, whether the result is tracked, volume, paging stability and scope duration.

open as a page

What changes for callers when data-access interfaces are defined one per table instead of one per object graph?

level: middleimportance: should knowfreq 52%

basics

~20 s

Slicing by table pushes assembly onto callers: they combine the cluster themselves, order the writes, propagate generated keys, hold the cross-object rules, and open the transaction, so the fetch plan ends up spread across call sites.

open as a page

A team defends its data-access interfaces by saying the store can be swapped underneath. How would you evaluate that claim?

level: principalimportance: should knowfreq 44%

basics

~20 s

Interfaces preserve method names and result shapes, not semantics. Write-back of tracked objects, multi-call atomicity, conflict behaviour, ordering stability and latency do not travel. Keep the boundary for naming, fetch-plan ownership and testability, not portability.

open as a page