skip to content

What do read paths that project lose from the mapped model's invariants, and how do you stop the two drifting?

level: principalimportance: should knowfreq 43%

answer

  1. a projection reads columns, not behaviour
  2. the rule now exists twice
  3. a missing predicate is the silent one
  4. pin both paths with one fixture

basics

~20 s

They lose behaviour the mapped model carried - derived values, rounding rules, visibility and soft-delete predicates - and must re-express it in the statement. The rule then exists twice, and only a shared test keeps the copies honest.

solid answer

~50 s

A projection reads columns; it cannot call the model. Anything the model computed - a status derived from two fields, a total with a rounding rule, an exclusion of soft-deleted or other-tenant rows that the load path applied for you - has to be written again inside the query. That duplication is the real cost of a projected read path, and the dangerous half is the predicate rather than the arithmetic: a wrong total is visible, a missing visibility filter leaks rows quietly. The defences are ordinary engineering. Keep the rule in one place where you can - a view, or a reviewed query fragment both paths share. Where it must be duplicated, pin it with a test that runs both paths over the same fixture and asserts they agree. And keep an inventory, because uncontrolled per-screen projections are how a filter gets forgotten.

go deeper

for a junior

The point to hold on to: a projection reads columns, so anything the model used to work out for you has to be written again in the query, and any filter it applied silently has to be written again too.

for a middle

Be able to name what is lost - derived values, applied predicates, type discipline, structural guarantees - and describe the cross-path test that keeps the two implementations agreeing.

for a senior

Show that you rank the risks: a wrong total is visible, a missing visibility predicate is not. Put the security-relevant rule somewhere structural rather than trusting a copied query.

for a principal

This is an inventory and ownership problem. Decide how many read shapes exist, who owns them, whether reads may cross aggregates, and which rules must never be duplicated at all.

## What was actually living on the mapped model When a read goes through mapped objects, several things come along free that nobody wrote down: - **Derived values.** A status computed from two columns, a total with a rounding or currency rule, a display name assembled from parts. - **Predicates the load path applied.** Soft-delete exclusion, tenant scoping, an archived-rows filter - often configured once on the mapped class or applied by a shared interceptor, and therefore invisible at every call site. - **Type and unit discipline.** Money as an amount plus a currency, a duration in known units, an enumeration mapped from a stored code. - **Structural guarantees.** A child collection is never null; a required link is always populated; an invariant holds across two fields because the constructor enforced it. A projection has access to none of that. It reads columns and builds a flat carrier. Everything above must be re-expressed in the select list, in the predicate, or in the code that consumes the result. ## The three ways this goes wrong 1. **Silent divergence.** The rule is copied correctly on day one and drifts on day ninety, when the model's version changes and the query's does not. Nothing fails; two screens simply show different numbers, and the argument about which is right takes a week. 2. **A missing predicate.** This is the serious one. If soft-delete or tenant scoping was applied by the mapped load path, a hand-written projection that omits it returns rows the caller must never see. It looks like a working feature, and it is a data-exposure bug. 3. **A read carrier used as a domain object.** Someone adds behaviour to the flat type, or tries to write through it. It has no invariants, no key discipline, and no write path, so the behaviour is wrong by construction. ## Keeping the rule single | Approach | What it gives you | What it costs | |---|---|---| | A database view both paths read | One definition, enforced by the engine | Schema-managed, versioned with migrations, harder to parameterise | | A shared, reviewed query fragment | One text for the predicate or expression | Only as good as the discipline that everyone uses it | | Recompute in one shared function fed by the projection | Reuses the model's logic exactly | Only works for row-local rules; needs the inputs selected | | Duplicate, pinned by a cross-path test | Cheap and honest about the duplication | The test is now load-bearing and must not be deleted | | Do not project this read | No duplication at all | Pays the full mapped-object cost on a read path | The order to try them in follows the risk. A **security or visibility predicate** should be structural - a view, or a fragment nobody can forget - because a test only catches what someone thought to write. Arithmetic can usually live with duplication plus a test. ## The cross-path test The most useful single control is small: build one fixture, run the mapped path and the projected path over it, and assert the two produce the same values. It catches divergence at the moment it is introduced rather than at the moment a customer notices. It is worth writing even where the projected path exists purely for speed, because that is exactly the case where nobody re-reads the query. ## The judgment calls a lead owns - **How many read shapes.** One per screen is a smell at scale, because each is a place a predicate can go missing. One per use case, owned by that use case, with an obvious home, is defensible. - **Where the read path may cross an aggregate.** Allowing it makes screens fast and quietly couples them to another team's tables. Decide, and write the decision down. - **When not to project at all.** If the rule is genuinely complex and only the model expresses it correctly, the mapped path is the cheaper engineering answer even where it is the slower runtime answer. - **What is not expressible.** Invariants that span rows - a limit across a customer's whole order history, an ordering constraint over a collection - cannot be re-expressed in a select list at all. A projection may report on them, but it cannot enforce them, and any code that treats a projected value as a guarantee is mistaken. ## What to watch for over time - Projections copied from one screen to another with the filter edited by hand. - A rule changed in the model with no corresponding query change in the same change set. - Read carriers that have grown methods. - Predicates present on some projections and absent on others - the fastest way to find this is to enumerate every projection and diff their `WHERE` clauses against the one the mapped path applies.

  • Which duplication is worth structurally preventing rather than testing?
    Visibility rules - tenant scoping, soft-delete, permission predicates. A test only checks the cases someone imagined, and a forgotten predicate exposes rows rather than showing a wrong number. Put those in a view or a shared fragment that a hand-written query cannot bypass.
  • Can a projection enforce an invariant that spans several rows?
    No. A select list can report a value computed across rows, but enforcement needs the write path - a constraint, or logic in the transaction that changes the data. Treating a projected number as a guarantee is a classic read-then-write race dressed up as a rule.
  • How do you keep the number of read shapes from exploding?
    Scope them to use cases rather than screens, give each an owner and a test, and require any new one to justify why an existing shape will not do. The count itself is a useful metric: each shape is a place a predicate can be forgotten.

saying these in an interview costs you the question

  • Assumes a rule expressed twice stays the same because it is simple
  • Copies another screen's projection and edits the filter by hand
  • Omits the tenancy or soft-delete predicate from a hand-written read
  • Adds behaviour to a flat read carrier as if it were a domain object
  • Treats a projected value as an enforced invariant
  • Claims projecting always beats the mapped path, whatever the rule