skip to content

When a service caches a finished transfer model above the mapper, what does the entry hold?

level: juniorimportance: should knowfreq 50%

answer

  1. a copy, not a live object
  2. nothing is watching it
  3. detached, serialized, fully materialized
  4. edits go nowhere; writes go through the mapper

basics

~20 s

A plain, already-assembled snapshot of a read result, usually serialized. It is detached: no unit of work tracks it, editing it writes nothing to the database, and it shows the data as of the moment it was built.

solid answer

~40 s

It holds a **copy**, not a live object. The service loads through the mapper, assembles the transfer model the caller actually needs, and stores that finished shape under a key — normally serialized to bytes or text. Nothing tracks the entry: there is no unit of work behind it, no dirty checking, no deferred link that can still reach the database, and no promise that two hits hand back the same instance. Mutating a cached model changes only that copy in the caller's memory. Writes still go through the mapper inside a transaction, and the entry is replaced or dropped afterwards. Being inert is the point: an entry nothing can mutate through is safe to hand to concurrent readers, and in most deployments to other processes as well.

go deeper

for a junior

Remember the one-liner: the entry is a detached copy of a finished result. Changing it changes nothing in the database, and it can be stale from the moment it is stored.

for a middle

Explain why it must be fully materialized before storage — no unit of work survives to complete a deferred part — and why the copy is serialized rather than shared as an object.

for a senior

Show that you place the store after the commit and treat the entry as an output of the write path, so a caller never sees a value the database rejected.

for a principal

Frame the layering: caching answers above the mapper trades identity and change detection for something shareable across processes, and moves freshness responsibility into your own code.

## Where this entry sits A data-access layer already keeps copies of rows after a read. In a layer that tracks the objects it loaded, one unit of work keeps a map of them, so a second lookup of the same identifier returns the same instance. Some mappers add a further tier shared beyond a single unit of work, holding row data that any later unit of work can re-hydrate from. Both of those live **inside** the mapper. An **application cache boundary** sits above all of it. The service that owns a use case loads through the mapper, builds the finished **transfer model** the caller asked for, and stores that under a key of its own choosing — typically in a store outside the process, so every instance of the service shares it. The difference is what the entry is made of: - the mapper's tiers cache **rows and managed objects** — material the mapper can still map, track, and complete on demand; - an application entry caches **an answer** — the exact shape the caller receives, after the joins, filtering, aggregation and projection have already run. ## What the entry actually contains - Every field of the assembled model, flattened into whatever transport form the store accepts — usually a serialized string or byte array, not a live object graph. - Anything computed while assembling it: totals, counts, derived flags, labels stitched in from other tables. - The key's own metadata if the design carries it inline — a shape version, the tenant it belongs to. And, deliberately, nothing that points back at the database: no stand-in for a deferred link, no reference to a unit of work, no connection. When the entry is written the mapper's involvement is already over. ## What "detached" costs and buys | Property | Object the mapper loaded | Cached transfer model | |---|---|---| | Instance identity | Same identifier returns the same instance within one unit of work | Each hit yields a freshly deserialized object | | Deferred links | A stand-in can complete itself on first access | Nothing left to load from; missing parts stay missing | | Change detection | Modifications are noticed and flushed | Modifications go nowhere | | Lifetime | Bounded by the unit of work | Independent of it; bounded by its own expiry | | Shared safely | No — shared mutable state tied to an open unit of work | Yes, when each caller deserializes its own copy | The right way to read that table is that the losses are the price of the last row. A managed object is useful precisely because something is watching it, and something watching it is exactly what makes it unsafe to hand to another request, another thread, or another machine. Copying it out and serializing it severs every one of those threads at once. ## The read and write path 1. Compute the key and look it up. On a hit, deserialize and return — no connection is taken, no transaction is opened, the mapper is never invoked. 2. On a miss, open the unit of work, load and assemble the model completely, and let the unit of work close. 3. Once the transaction has committed, store the finished model under the key. 4. On a write, change the record through the mapper as normal, and refresh or remove the entry afterwards. The entry is an **output** of the write path, never an input to it. Two consequences of step 2 catch people out. The model must be **fully materialized** before it is stored: any part left as a deferred stand-in cannot be completed later, because by then there is no unit of work behind it. And whatever the caller does to the returned object has no persistence meaning at all — an edit that "did not save" is the single most common surprise here, and it is not a bug in the cache. ## Why services do this at all Mapper-level caching answers "fetch this row again without a query". It cannot answer "give me the finished dashboard payload for this account without doing the assembly again", because it does not store answers — it stores rows. When the expensive part of a request is the assembly rather than the fetch, the useful entry is the assembled result, and that result is a plain snapshot by construction. The trade is honest: you buy a cheap, shareable, transport-ready answer, and you give up identity, deferred completion, and change detection. For a read path that just returns data, none of those three were being used anyway.

  • Why is caching the mapper's own loaded objects, rather than a copy, usually a bug?
    Those objects belong to a unit of work that ends. Once it does, any deferred part of the graph can no longer load, and the objects become shared mutable state that two requests can modify at once. A serialized copy has neither problem: it is inert, and every caller deserializes its own.
  • If a cached model is inert, how does a write path ever change what readers see?
    Through the mapper, inside a transaction, exactly as if no cache existed. The cache is then updated or the key dropped once the commit returns. The entry never participates in the write; it only records what the read path last produced.

saying these in an interview costs you the question

  • Thinks editing a cached transfer model will be saved to the database
  • Expects a deferred link to load when touched after a cache hit
  • Assumes two hits for one key return the same object instance
  • Caches the mapper's tracked objects instead of a copy of them
  • Believes the entry refreshes itself when the underlying row changes