What changes for callers when data-access interfaces are defined one per table instead of one per object graph?
answer
- who assembles the cluster
- insert order and generated keys
- where the fetch plan lives
- the shared scope hides the seam
basics
~20 sSlicing 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.
solid answer
~50 sSlicing by table gives you small uniform interfaces and moves the assembly work upward. Each caller loads the parent and the children separately, decides the insert order, copies the generated parent key down, enforces whatever rules span the cluster, and needs someone above the layer to open one transaction around the several calls. The practical cost is that the fetch plan is now wherever the calls happen, so there is no single place to change it. Slicing by object graph puts all of that inside one boundary, at the price of a method list that grows with use cases and load methods that can pull more than a screen needs. Note that a shared unit of work hides much of the difference — it may order writes and flush everything tracked — so a per-table design can look fine until a call path stops sharing that scope.
go deeper
Know the two ways a data-access layer gets sliced - one interface per table, or one per cluster of objects saved together - and that the slicing decides how much stitching the calling code does.
Explain the concrete work that moves: assembling the cluster, insert order, generated keys, cross-object rules, and opening one transaction over several calls. Name the cost of a scattered fetch plan.
Show you can spot the design being carried by a shared scope rather than by its interfaces, and say what breaks when a call path stops sharing that scope.
Argue the tradeoff: uniform narrow interfaces versus a growing per-graph method list, and where read shapes belong so the write boundaries do not absorb every reporting need.
## Two ways to slice the same layer A data-access layer can be cut along the **table** - one interface per stored table, each with the usual find, save and delete methods - or along the **object graph**, one interface per cluster of objects that is loaded and saved as a unit. The two look similar from inside the layer. They differ a great deal for the code above it, because the slicing decides which of the assembly work the caller has to do itself. ## What a per-table boundary hands to the caller - **Assembling the cluster.** A screen that needs a parent and its children makes several calls and stitches the result together. The knowledge that these rows belong together now lives in the caller, once per caller. - **Write ordering and key propagation.** Parent before child, and the generated parent key copied into the children, unless a shared unit of work is ordering the writes on everyone's behalf. - **Consistency rules across the cluster.** Anything that must hold between parent and children - totals, counts, at-least-one rules - is enforced by whoever happens to be calling. - **A scattered fetch plan.** How much is loaded for a given use case is decided by which calls the caller happens to make, so there is no single place to change it when it turns out to be wrong. - **Transaction demarcation.** Several calls have to be one atomic write, so someone above the layer must open the scope. The boundary itself no longer marks the unit of change. ## What the tracked set quietly covers up On a layer with a unit of work, per-table boundaries often look fine for a long time. Saving one part flushes every tracked change in the scope, so a caller that forgot to save the children may still see them written; the layer may also order inserts and fill in generated keys itself. The design is being carried by the shared scope rather than by the interfaces. The cover comes off when the scope is not shared: a batch job that opens its own scope per chunk, a call path that reaches the layer twice from different transactions, or a move to a layer with no tracked set at all, where each call really is exactly the statement it looks like and ordering is back on the caller. ## What a per-graph boundary buys, and what it costs | Concern | One interface per table | One interface per object graph | |---|---|---| | Assembling a cluster | Caller, at each call site | Boundary, once | | Write order and key propagation | Caller, or the shared scope | Boundary | | Where the fetch plan lives | Spread across call sites | One place per graph | | Method count | Small, uniform, generic | Grows with each use case | | Cross-graph reads | Fit naturally | Do not fit; go elsewhere | | Over-fetching risk | Lower per call | Higher, if the graph loads whole | The costs are real. A per-graph boundary tends to grow a wide method list as use cases accumulate, and a load-the-whole-cluster method pulls more rows than a list screen wants. Both are worth watching, and neither is a reason to go back to slicing by table. ## The queries that fit neither Some reads span several graphs, or aggregate across them, and there is no honest owner among the graph interfaces. Two things usually happen. The healthy outcome is a separate read-oriented boundary that returns a transfer model shaped for the screen and makes no claim to own any graph. The unhealthy one is the query drifting into the service that needed it, where it is invisible to everyone else: the next service writes its own near-copy, and the day the fetching has to change there is no single place to change it. A useful rule of thumb: the moment a service is building queries instead of calling named ones, the boundary has stopped covering a real use case, and the fix is a named method or a named read boundary - not a slightly wider generic finder that every caller then composes against. ## Choosing in practice 1. **Slice by what is written together.** If two things must be saved atomically and their rules are checked together, they belong behind one boundary. 2. **Let reads have their own boundary.** Read shapes multiply faster than write clusters and do not need the same slicing. 3. **Check the assumption about the shared scope.** If a per-table design only works because one scope is open around everything, write that down as a precondition or remove the dependency. 4. **Watch the method list.** A graph boundary whose methods are all generic has been sliced by table under a different name.
- Why can a per-table design look correct for years and then break in a batch job?Because a shared unit of work was doing the assembly work. One open scope orders the inserts, fills generated keys and flushes every tracked change, so callers appear to be saving correctly. A job that opens a scope per chunk, or a path that touches the layer from two transactions, removes that cover and the missing ordering becomes visible.
- Where should a read that spans three unrelated graphs live?On a read-oriented boundary of its own, returning a transfer model shaped for the screen. Hanging it on whichever graph interface is largest makes that interface own data it does not own; letting it drift into the service that needed it leaves the fetching with no single place to change.
saying these in an interview costs you the question
- Says interface granularity only affects how many files the layer has
- Assumes the caller never has to order writes because the layer always sorts it out
- Puts a cross-graph report query on whichever graph interface is biggest
- Believes a per-graph boundary must always load the whole cluster
- Treats a generic finder every service composes against as a real boundary