A team defends its data-access interfaces by saying the store can be swapped underneath. How would you evaluate that claim?
answer
- syntax travels, semantics does not
- write-back needs a unit of work
- atomicity belongs to the store
- real swaps are partial
- keep the boundary, drop the reason
basics
~20 sInterfaces 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.
solid answer
~50 sSeparate what a caller depends on into syntax and semantics. Method names, parameters and transfer model shapes survive a swap by construction. Semantics mostly does not: silent write-back of changes to returned objects needs a unit of work the next store may not have, multi-call atomicity depends on the store's transaction reach, conflict and constraint failures surface at different moments, and ordering, paging stability and latency follow the store's own model. The write-back row usually settles the argument — every caller relying on it must be rewritten, which is exactly what the boundary was supposed to prevent. Real swaps are also partial: one graph to a search index or an archive, not the whole store. So keep the boundary for its weekly benefits — named queries, one place for the fetch plan, testable call paths — and drop portability as the justification.
go deeper
Understand the claim being made: an interface lets a different implementation be substituted. Know that this covers method names and result shapes, and not how fast or how atomic the operations are.
Explain why semantics does not travel. Give two concrete examples, such as changes to returned objects being written automatically, and two calls landing as one atomic write.
Show what you would check in the codebase: call sites relying on write-back, scopes spanning several calls, ordering assumptions in paging, and any layer query type on the interface.
Make the call. Say which benefits of the boundary are collected weekly and which are speculative, what portability costs up front, and when it is worth designing for one named partial swap instead.
## What the claim actually promises "The store can be swapped underneath" means: an implementation of these interfaces could be written against a different store, and the code above would keep working unchanged. It is worth taking seriously, because a boundary that really delivers it is cheap insurance, and worth interrogating, because most boundaries deliver much less than the claim implies. Start by separating the two things a caller depends on. The **syntax** of the boundary is its method names, parameters and return shapes. The **semantics** is everything the calling code assumes about what happens when it calls: when the write lands, what is atomic, whether the result is live, what error arrives on a conflict, how long an operation takes. Swapping implementations preserves the syntax by construction. It preserves the semantics only where somebody made that a requirement. ## What travels, and what does not | Caller-visible property | Survives a swap? | Why | |---|---|---| | Method names and parameter lists | Yes | The interface is the artefact being reimplemented | | Transfer model shapes | Yes | Plain values the new implementation can also produce | | Lookup by key returns at most one | Usually | A property of the model, not the store | | Write-back of changes to returned objects | Rarely | Needs a unit of work the new store may not have | | Multi-call atomicity | Rarely | Depends on the store's transaction support and reach | | Conflict and constraint failures | Rarely | Different stores detect them at different moments | | Ordering and paging stability | Rarely | Follows the store's ordering and concurrency model | | Statement count and latency shape | No | A property of the implementation, always | | Filter and sort expressiveness | No | What one store can answer, another cannot | The row that ends most portability arguments is write-back. If callers mutate returned objects and rely on the change being written at commit, they depend on a unit of work. A store without one cannot offer that, and there is no adapter that fakes it convincingly. Every such caller has to be rewritten to make its writes explicit, which is the change the boundary was supposed to prevent. The second most expensive row is atomicity. Code that quietly relies on two boundary calls landing together assumes a transaction that spans both. Whether a replacement store can honour that - or honour it across the same span - is a property of the store, not of the interface. ## When portability is a real requirement It usually is not the scenario people imagine. Whole-store migrations are rare and are rarely made easier by an interface. What does happen: - **Moving one graph to a different store** while everything else stays put - a search index, an archive, a queue-backed write path. - **A read replica or an alternate read path** for reporting. - **A second implementation for tests**, useful, though it introduces a divergence problem of its own. All three are partial. They argue for a boundary shaped around *one* graph or *one* read, not for a uniform layer justified by a migration nobody has scheduled. ## What you actually pay to get it 1. **Return snapshots, not tracked objects.** No caller may depend on a scope watching a returned value, so writes become explicit calls. 2. **Keep the layer's own query types off the interface.** The moment callers compose filters in the layer's vocabulary, the vocabulary is the contract. 3. **Make the transaction boundary explicit and coarse.** One boundary call per unit of change is portable; a scope quietly spanning six calls is not. 4. **Write contract tests once and run them against every implementation**, including ordering, conflict behaviour and empty-result semantics - the parts that silently differ. 5. **Restrict yourself to the intersection of what the candidate stores can do**, and accept that this leaves capability on the table in the store you actually use. ## How to answer the team The honest evaluation is: the boundary is worth having, but not for this reason. It earns its keep by giving each query a name and a single owner, by putting the fetch plan in one place, by keeping query mechanics out of business code, and by making call paths testable. Those benefits are collected every week. Portability is collected once, if ever, and only if the five costs above were paid in advance. So keep the boundary, drop the justification, and if portability really is a requirement, name which store and which graph - then design for that swap specifically rather than for a generic one.
- Which single caller habit most reliably destroys store portability?Mutating a returned object and relying on the change being written at commit. That habit depends on a unit of work watching the object, which a store without one cannot provide and no adapter can convincingly fake. Every such call site becomes an explicit load-modify-save rewrite during the swap - the work the boundary was meant to avoid.
- If portability really is required, what does the boundary have to give up?Convenience. Snapshots instead of tracked results, explicit writes, no layer query types on the interface, one call per unit of change, and a feature set limited to the intersection of the candidate stores. Plus a contract test suite covering ordering, conflicts and empty results, run against every implementation.
- What are the defensible reasons to keep the boundary once portability is off the table?Each query gets a name and one owner, the fetch plan for a use case lives in one place, business code stays free of query mechanics, and call paths become testable and assertable. Those pay off continuously, unlike a migration that may never be scheduled.
saying these in an interview costs you the question
- Assumes an interface makes the store swappable by itself
- Ignores that write-back of tracked objects needs a unit of work
- Believes multi-call atomicity is a property of the interface
- Expects conflict and constraint errors to surface identically everywhere
- Plans for a whole-store migration nobody has scheduled
- Exposes the layer's query type and still calls the boundary portable