For a mass data change, how do you decide between a set-based statement through the layer and the object path, and who then owns the skipped invariants?
answer
- is the new value derivable in the database
- enumerate skipped invariants against this change
- speed is bought with transferred ownership
- a second write path needs a home
basics
~20 sDecide on two axes: whether the new value is derivable from data the database already holds, and whether the invariants the object path enforces are load-bearing. The statement path transfers those invariants to you — explicitly, tested and documented.
solid answer
~50 sStart with feasibility: if each row's new value depends on logic that lives only in application code, the statement path is not available and the question is moot. If the change is expressible as a predicate plus an assignment, the real decision is about the guarantees you drop — cascades, lifecycle hooks and the events or audit rows inside them, the per-row version check, and the freshness of anything already loaded or cached. For each, ask who reproduces it: the schema, an explicit statement, or a deliberate accepted loss. Then treat the result as a **second write path** and manage it as one: put it next to the object path, test the invariant rather than the callback, and make the ownership visible in the code, because the failure mode is not today's change — it is the hook someone adds in six months that the bulk path silently does not run.
go deeper
Understand the shape of the choice: one statement is far faster, and the layer's per-object work does not run, so someone has to cover that work.
Be able to enumerate what is skipped — cascades, hooks, events, the version check, freshness of loaded and cached copies — and propose a concrete compensation for each.
Drive the decision from the invariants rather than from throughput, and show how you keep the two paths from diverging over time through co-location and invariant-level tests.
Own the standing policy: whether bulk write paths are permitted, where they live, what each must document, and how the system keeps a single answerable story about what happens when a row changes.
## The decision is not "is it faster" It almost always is. The interesting question is what the speed costs and whether the organisation can carry it. ## Step 1 — is the statement path even available? A set-based write requires the new value to be derivable from data the database already has: a constant, an expression over the row, or a value from a joined table. If the new value depends on application logic — a rules engine, a call to another service, anything not expressible in the query language — you cannot express it in one statement. The honest options are then the object path, or staging the computed values in a table and applying them in one statement afterwards, which is the statement path with an extra step. ## Step 2 — inventory what is skipped, per invariant Do not reason about "guarantees" in the abstract; enumerate them against this change. | Skipped behaviour | The question to ask | If it matters | |---|---|---| | Cascade to children | Do dependent rows need to change too? | Declare the referential action in the schema, or issue explicit child statements in dependency order | | Lifecycle hooks | Does a hook write audit rows, stamp fields, or maintain a derived column? | Reproduce it set-based over the same predicate | | Events published on change | Does a downstream consumer — search index, cache, another service — rely on per-row notifications? | Emit an equivalent signal, and agree with consumers whether one summary event suffices | | Per-row version check | Could a concurrent writer be mid-change on these rows? | Narrow the predicate, or take the object path for the contended subset | | In-memory and cached copies | Who holds copies of these rows right now? | Order the bulk statement before loading, evict what it touched, and invalidate caches deliberately | Most changes have one or two live entries and several that genuinely do not apply. Writing the table out is what turns a vague unease into a decision. ## Step 3 — weigh the operational footprint The object path spreads the work over many small transactions; a single statement over millions of rows concentrates it. That concentration has its own costs on a live system — a long write transaction, a large volume of change to replicate, and contention with concurrent traffic on the same rows. The mitigations belong to the statement's own design rather than to the layer, but a decision made without acknowledging them is incomplete. Conversely, the object path's cost is not just time: it is millions of small writes, each with its own overhead, spread over a window in which the system is in a partially-updated state that concurrent readers can observe. Neither path is inherently safer for a live system; they fail differently. ## Step 4 — accept ownership explicitly Once you choose the statement path, the skipped behaviour becomes yours. Three practices keep that honest: 1. **Co-locate.** Put the bulk operation in the same module as the object path for the same entity. A change to the neighbourhood is then visible to whoever extends either one. 2. **Test the invariant, not the callback.** Assert that no orphaned children remain, that the audit trail covers every affected row, that the search index converges. A test asserting a hook fired passes on the object path and never exercises the bulk one, so it cannot detect divergence. 3. **Document the loss where it was chosen.** "This path deliberately does not publish a per-row event; consumers receive one batch signal instead" is a sentence that survives staff turnover. An undocumented omission reads as a bug to the next person, who will either re-add the cost or work around it. ## Step 5 — the standing question, not the one-off The strategic version of this decision is whether the codebase should have two write paths for the same rows at all. A team that reaches for bulk statements freely accumulates a shadow set of write paths that no hook, no cascade and no event applies to, and eventually nobody can say what actually happens when a row changes. A team that forbids them entirely writes loops that cannot finish inside a maintenance window. The workable middle is to make it a small, visible category: bulk operations are allowed, they live in named places, each carries an explicit note of what it skips and how that is compensated, and adding one is a decision someone reviews rather than a local optimisation. The point is not the ceremony — it is that "how a row changes in this system" stays answerable.
- How do you handle a mass change whose per-row value comes from application logic?Compute the values once and stage them in a table keyed by identity, then apply them with a single statement that joins the staging table to the target. You keep the application logic and still get one set-based write, at the cost of an extra load step and a table to clean up.
- Which test detects that a bulk path has drifted from the object path?One that asserts the invariant rather than the mechanism: after running the bulk operation, no orphaned children exist and the audit trail covers every affected row. Tests that assert a callback fired only ever exercise the object path, so they stay green while the bulk path diverges.
- When is the object path the right choice even though it is far slower?When the invariants enforced per object are load-bearing and expensive to reproduce faithfully — several cascades, hooks other teams depend on, a genuine need for per-row concurrency detection — and the volume still fits the available window. Slow and correct beats fast with a compensation scheme nobody maintains.
saying these in an interview costs you the question
- Chooses the bulk path purely on speed with no inventory of what it skips
- Assumes downstream consumers can absorb missing per-row events without asking
- Leaves the compensation undocumented so the next reader reads it as a bug
- Treats one huge transaction as free because it is a single statement
- Believes the object path is always the safe default regardless of volume