skip to content

A request drives a tracking mapper and a separate row-mapping layer side by side; what must be shared and what goes stale?

level: seniorimportance: should knowfreq 45%

answer

  1. one connection, one transaction
  2. single owner of commit
  3. the tracked set did not see it
  4. stale objects get written back
  5. route each table to one owner

basics

~20 s

Both layers must run on one connection inside one transaction with a single owner of commit and rollback. Anything the mapper already loaded is stale once the other layer writes those rows, and must be discarded or re-read.

solid answer

~40 s

Mixing is fine; two connections are not. On separate connections you have two transactions that can half-commit, cannot see each other's uncommitted work, and can deadlock against each other on the same rows while the request waits for a lock timeout. So share one connection and let exactly one component own the transaction boundary. The second hazard is staleness: the tracked set knows nothing about statements it did not emit, so objects loaded before a raw write still carry the old values, and dirty checking will happily write them back over the raw change. Drop or re-read anything the other layer touched, and push pending changes before the other layer reads the same rows.

go deeper

for a junior

Take away the rule of thumb: two data-access layers in one request must sit on the same connection, and one component decides when to commit.

for a middle

Explain why separate connections break things — two transactions cannot see each other's uncommitted rows and can block each other on the same locks.

for a senior

Show the diagnosis: a request that hangs to a lock timeout, or a raw update that vanishes because a stale tracked object was written back over it.

for a principal

Set the policy: one writer per table per request, split by direction, and a transaction boundary owned above both layers rather than negotiated between them.

## Why teams end up with both Mixing is normal, not a smell. A team keeps the full mapper for the write model, where a tracked set and cascade earn their keep, and reaches for a query builder or a row-mapping layer for the reporting queries, the bulk import and the export. The problem is that the two layers are two independent pieces of machinery, and unless you arrange otherwise, each will happily obtain its own connection and open its own transaction. ## What must be shared: one connection, one transaction If the mapper writes on connection A and the row-mapping layer writes on connection B, you no longer have one transaction — you have two, and everything that follows from that is bad: - **Half-commit.** One can commit while the other rolls back. The request either succeeds partially or leaves the data in a state no code path expects. - **Invisibility.** Under normal isolation, uncommitted work on connection A is not visible to a read on connection B. The second layer reads the *pre-change* row and behaves as if the first layer had never run. - **Self-deadlock.** Both connections want write locks on the same rows. Connection B waits for A, and A cannot proceed until the request finishes, which it cannot do while B is blocked. The request hangs until the lock timeout fires — and it will pass every test that exercises only one of the two layers. So the first requirement is mechanical: both layers must run on the same connection for the duration of the request, and exactly one component must own begin, commit and rollback. Whether that owner is the mapper's transaction scope or a boundary you wrote is a design choice; that there is only one owner is not. ## What goes stale: everything already loaded Even on a shared connection, the mapper's tracked set knows nothing about a statement it did not emit. Two directions of staleness matter, and they matter differently: | Situation | What the mapper believes | What you must do | |---|---|---| | A raw statement updated rows the mapper had loaded | Its objects still hold the pre-update column values | Discard or re-read those objects; do not write them back | | A raw statement deleted rows the mapper had loaded | The objects still look alive | Drop them; a later write would resurrect or fail | | The mapper has pending changes not yet sent | They are safely recorded | Push them before the other layer reads the same rows, or accept reading the old values | | Any cache built over the untracked layer | Nothing — the mapper never told it | Invalidate it yourself at the write site | The nastiest version is the first combined with a later mapper write: the tracked object still carries the old values, dirty checking compares against the old snapshot, and the mapper writes stale columns straight over the raw update. The raw write appears to succeed and then silently vanishes. ## An arrangement that works 1. Own the transaction in one place, above both layers, and inject the same connection into each. 2. Sequence the request so that raw work happens either entirely before the mapper loads anything, or entirely after the mapper's pending changes have been pushed. 3. Treat objects that a raw statement touched as invalid from that point on: drop them, or re-read them and use only the fresh copy. 4. Never let the two layers write the same table in the same request unless one clearly owns it — pick an owner per table and route the write through it. 5. Invalidate any hand-built cache at the write site, because there is no flush event to hang it on. ## What interviewers listen for The strong answer names both hazards, and in the right order: **the transaction and connection must be shared, or nothing else you say matters**, and then **anything already loaded is stale after the other layer writes**. Weaker answers stop at *it might be slow* or assert that a transaction manager will automatically reconcile two connections. It will not: coordinating two connections is a distributed-transaction problem, and adopting one to solve an in-process ordering issue is a much larger commitment than reusing one connection. A last point worth making: mixing is easiest to keep safe when the split is **by direction rather than by table** — the mapper owns writes to a table, the untracked layer owns heavy reads over it — because the staleness window then only opens in one direction, and read results are consumed and discarded rather than written back.

  • Why can two connections in one request deadlock against themselves?
    Each connection is a separate transaction holding its own write locks. If the second wants a row the first has locked, it waits, and the first cannot release until the request completes — which it cannot do while the second is blocked. Nothing resolves it until a lock timeout fires.
  • Which direction of staleness causes silent data loss rather than a visible error?
    A raw statement updating rows the mapper already holds. The tracked object keeps the pre-update values, so a later dirty-checking write compares against the old snapshot and overwrites the raw change with stale columns. The raw write succeeds, then disappears, with no error anywhere.
  • How do you split responsibility so the mix stays safe?
    Split by direction, not by table: let the mapper own writes for a table and let the untracked layer own heavy reads over it. Read results are consumed and discarded rather than written back, so the staleness window only opens one way and no table has two writers in one request.

saying these in an interview costs you the question

  • Expects a transaction manager to reconcile two separate connections
  • Believes uncommitted work on one connection is visible on another
  • Assumes the tracked set notices statements it did not emit
  • Writes back a loaded object after a raw statement changed its row
  • Treats a hanging request as slowness rather than a self-inflicted lock wait