What does an application cache of transfer models give up that the mapper's own cache keeps?
answer
- rows versus finished answers
- no map, no stand-ins, no snapshot
- identity, deferred links, change detection
- assembly cost is what you actually save
- shape becomes part of the key's contract
basics
~20 sIdentity, deferred completion and change detection. A cached answer is a detached snapshot with nothing behind it, so no shared instance, no link that can still load, no dirty checking. In exchange it skips the whole assembly path and travels between processes.
solid answer
~50 sThree things, and giving them up is usually the point. First **identity**: the mapper's map returns one instance per identifier within a unit of work, while every cache hit deserializes a fresh object. Second **deferred completion**: a stand-in can fetch its data on first access only while a unit of work is behind it, so an application entry must be fully materialized before it is stored. Third **change detection**: nothing compares the cached copy against a loaded snapshot, so edits are invisible and no optimistic version check protects it. What you buy is the part the mapper's tiers cannot give you — the assembly is already done. A mapper tier caches rows; it still has to map, stitch and project them on every hit. An application entry caches the finished answer, in a form other processes can read.
go deeper
Learn the three losses by name — identity, deferred completion, change detection — and that a cached model is a snapshot with nothing behind it.
Explain mechanically why each loss follows from the entry being detached and serialized, and state what is bought in exchange: the assembly, not just the query, is skipped.
Diagnose from the cost profile. Identify whether a slow read is dominated by fetching rows or by assembling them, and pick the layer that removes the dominant cost.
Weigh the ongoing tax: an application entry buys the biggest saving per hit and hands your team the freshness and shape-versioning problem the mapper used to own.
## Two caches, two units of currency Both layers avoid work, but not the same work. A mapper's own tiers store **row data and managed objects**. A hit still runs through the mapper's machinery: the row is turned into an object, registered in the current unit of work's map, wired up with stand-ins for whatever is deferred, and — in layers that do dirty checking — snapshotted so later modifications can be detected. That machinery is what makes the result useful for writing. An application cache stores **the finished answer**. A hit is a deserialization and a return. No mapping, no map registration, no snapshot, no stand-ins — and no way back to the database either. ## What is given up - **Identity.** Within a unit of work, loading the same identifier twice yields the same instance, so two parts of one request cannot disagree about the record. Cache hits give each caller its own deserialized object. Code that compares references, or that mutates one reference expecting another to see it, breaks. - **Deferred completion.** A stand-in for an unloaded association resolves itself on first access using the unit of work that created it. Nothing behind a cached entry can do that. Anything the caller may touch has to be materialized *before* the entry is written, which also means an over-eager entry can be much larger than the row it came from. - **Change detection.** Layers differ in how they notice a modification — some compare against a snapshot kept at load time, others intercept the change as it is made — and concurrent updates are protected by a version column carried in the update's where clause. A cached copy participates in none of that: it has no snapshot, and its version field, if it has one, is just data. - **Automatic upkeep.** A mapper knows when it wrote a row and can drop its own entry for it. A store outside the mapper knows nothing until your code tells it, which is why the write path grows an explicit step. ## What is gained | Cost paid on a read | Mapper-tier hit | Application-cache hit | |---|---|---| | Query to the database | Avoided | Avoided | | Row-to-object mapping | Still runs | Avoided | | Multi-table stitching and aggregation | Still runs | Avoided | | Projection into the caller's shape | Still runs | Avoided | | Serialization for transport | Still runs | Often avoided — stored in transport form | | Usable across processes | Depends on the tier's topology | Yes, when the store is external | The right question is therefore not "which cache is better" but **where the cost of this read actually is**. If a request is expensive because it fetches many rows one identifier at a time, the mapper's own tier is a good fit and maintains itself. If it is expensive because assembling the answer takes joins, aggregation and several stitched calls, no row cache helps: the rows were never the problem. ## Why the losses are usually acceptable Look at what a read path actually uses. It fetches data, shapes it, and returns it. It does not compare object references, does not lazily wander into associations after the fact, and does not modify anything. Identity, deferred completion and change detection are mostly facilities for *writing* — and the write path is not served from the cache at all. On a path that only reads, giving them up usually costs little. The losses bite in exactly two situations, both worth naming in an interview: 1. **A caller treats the cached model as a managed record.** It edits the object, expects a save, and gets silence. The fix is a type boundary: the cached shape should not be the same type the mapper manages, so the confusion is impossible to express. 2. **The model was not fully materialized.** Something reads a part that was left deferred, and instead of a lazy fetch it finds nothing — or the serialization step itself trips over the stand-in and stores something useless. The fix is to assemble deliberately, not to hand the serializer whatever the mapper returned. ## The shape rule that follows Because the entry is a snapshot in a transport form, its shape is part of its contract. A deploy that adds or renames a field changes what "the same key" means, and old entries written by the previous version will deserialize into something wrong or fail outright. Carrying a shape version in the key, so a change simply misses instead of mis-reading, is the standard defence — and it is a concern the mapper's own tiers never have, because they cache rows whose shape the schema already governs.
- If both layers are hits, which one returns faster, and does that settle the choice?The application entry, because it skips mapping, stitching and projection as well as the query. But it does not settle the choice: the mapper's tier invalidates itself on writes, while an application entry only changes when your code changes it. Speed on a hit is traded against upkeep on every write path.
- Why does an entry need a shape version in its key when the mapper's tiers do not?A mapper caches row data whose shape the schema defines and migrations govern. An application entry caches a serialized model whose shape is defined by code that redeploys independently, so entries written before a change can be misread after it. A version segment makes those keys miss instead.
- Does caching a fully materialized model ever make the read slower?Yes. Materializing associations the caller rarely touches inflates both the assembly on a miss and the payload on every hit, and a large value can cost more to transfer and deserialize than a narrow query would have cost outright. Cache the shape actually returned, not the whole graph.
saying these in an interview costs you the question
- Claims a mapper-tier hit also skips the mapping and projection work
- Expects the mapper to invalidate entries the application layer wrote
- Stores a model with associations still deferred and unresolved
- Relies on the version field in a cached copy to prevent lost updates
- Reuses the mapper's managed type as the cached shape
- Treats reference equality of two cache hits as meaningful