skip to content

When should an always-on tenant or live-rows filter be switched off for one unit of work, and what breaks?

level: seniorimportance: should knowfreq 47%

answer

  1. support view, purge, restore, migration
  2. narrow block, never a mode
  3. unfiltered objects stay in the tracked set
  4. a shared cache carries it further
  5. authorise and record the exception

basics

~20 s

Only for a narrow, explicit block that genuinely must see excluded rows — a support view, a purge, a restore. What breaks is downstream: unfiltered objects stay in the tracked set and reach code that still assumes the filter.

solid answer

~50 s

The exceptions are real: a support screen that must show a cancelled record, a job that purges or restores rows, a cross-tenant report. Layers therefore let the filter be disabled, and the only defensible scope is **one short unit of work that does nothing else**. The danger is that disabling changes what the layer *reads*, not what it *returns later*: an object loaded while the filter was off is now in the tracked set, so a subsequent lookup by key inside the same unit of work hands it back with no query at all, and if the layer populates a cache that outlives the unit of work, the excluded object can escape into other work entirely. So keep the block narrow, do not run ordinary business logic inside it, and never make the switch-off a process-wide setting or a flag threaded through service methods.

go deeper

for a junior

Know that legitimate exceptions exist — support views, purges, restores — and that the layer can lift the filter for them rather than the code bypassing the mapping.

for a middle

Explain why the switch-off must be scoped to one narrow unit of work, and what a mode-wide or process-wide toggle costs.

for a senior

Show the downstream effects you plan for: unfiltered objects sitting in the tracked set, a shared cache carrying them further, and lazily resolved references that depend on when they were touched.

for a principal

Treat the escape hatch as a governed capability: who may see hidden or cross-tenant rows, how that is authorised and recorded, and how the design keeps the exception from becoming the norm.

## Why a switch-off has to exist A filter that can never be lifted is a filter you will end up removing. Every system with hidden rows eventually needs to see them: - a support tool that must display a cancelled or deleted record to answer a customer; - a purge job that must find the very rows the read path hides in order to erase them; - a restore path that un-hides a row someone removed by mistake; - an operational report that spans tenants deliberately; - a data migration that must rewrite every row, visible or not. Refusing to provide the escape hatch does not make these disappear. It pushes them onto hand-written statements that bypass the mapping entirely, which is strictly worse: now the exception is invisible and unreviewable. ## The only sane scope | Scope | Verdict | |---|---| | One short, explicit block inside one unit of work | The right shape — the exception is visible where it applies | | A flag threaded through service method parameters | Poor — every caller can now pass it, and eventually one does by accident | | A per-request toggle driven by a query parameter or header | Dangerous — the exception becomes reachable from outside | | Process-wide, set at startup | Equivalent to having no filter, plus a declaration that misleads readers | | Per test suite, "so the fixtures work" | A sign the fixtures are wrong; it also stops the tests from proving the filter works | ## What actually breaks Disabling changes what the layer reads. It does not change what the rest of the code believes. 1. **The tracked set is poisoned for the rest of the unit of work.** An excluded object loaded while the filter was off is now held by the unit of work. A later lookup by key for that identifier is answered from memory, without a statement, so the predicate never gets a chance to exclude it. Code written after the block, expecting the filter, receives the row. 2. **A cache beyond the unit of work can carry it further.** If the layer populates a shared cache from what it loads, an object read with the filter off can be served later to work that never disabled anything. That is the failure that crosses request boundaries, and it is the strongest argument for not caching filtered types. 3. **Associations resolved later inherit the moment, not the intent.** A lazily resolved reference is filtered according to the state in force when it is resolved, which may be inside or outside the block depending on when the code touches it. Two runs of the same logic can differ. 4. **Writes inside the block become dangerous.** A modification to an object that should have been invisible is flushed as a perfectly ordinary UPDATE, and stamping hooks attribute it to the current actor. Restoring a row is a legitimate case of exactly this; doing it by accident is not. 5. **Cross-tenant reads are unbounded by definition.** Switching off a tenant filter turns a query that returned one customer's rows into one that returns everyone's, with the memory and latency profile to match. ## Designing the exception well - **Make it a named operation, not a mode.** "Purge deleted accounts older than N days" is a named operation that internally lifts the filter; "run this service with filters off" is a mode, and modes get reused. - **Give it its own unit of work.** Open it, do only the exceptional work, finish it. Nothing that runs ordinary business logic should share it. - **Read, then decide, then act on identifiers.** Where possible, gather the identifiers with the filter lifted, end that unit of work, and perform the work in a normal one. This keeps unfiltered objects out of the tracked set that the rest of the request uses. - **Authorise it separately.** The right to see hidden rows, and especially other tenants' rows, is a distinct permission from the right to use the feature. It should be checked, and the use should be recorded — a support tool that can read any customer's record is an audit subject in its own right. - **Test the exception as carefully as the rule.** One test that the block sees the excluded row, and one that work after the block no longer does. ## The judgement to show A candidate who says "we disabled the filter for the admin module" has described a mode. A candidate who says "the purge job opens its own unit of work with the filter lifted, collects identifiers, and everything else runs filtered" has described a boundary — and boundaries are what keep a safe default safe.

  • Why is a process-wide switch-off worse than having no filter at all?
    Because the declaration remains. Every reader of the mapping, and every reviewer of a query, assumes reads are narrowed, so nobody adds the predicate by hand. The system behaves as if unfiltered while its source claims otherwise, which is the most expensive kind of wrong.
  • How do you stop an object read with the filter lifted from leaking into ordinary code?
    Keep the exception in its own short unit of work and, where practical, return identifiers rather than objects. Ending that unit of work discards the tracked set, so the follow-up work re-reads through the normal filtered path. Be especially careful if the layer populates a cache that outlives the unit of work.
  • Should the ability to lift a tenant filter be governed by permissions?
    Yes, and separately from the feature it serves. Seeing across tenants is a distinct capability from using a support tool, so it warrants its own authorisation check and a record of who exercised it and over which data. Treat the exception path as an audit subject, not an implementation detail.

saying these in an interview costs you the question

  • Turns the filter off globally so one screen works
  • Threads an include-deleted flag through ordinary service methods
  • Assumes disabling only affects the query it wraps
  • Runs normal business logic inside the unfiltered block
  • Exposes the switch-off through a request parameter
  • Never tests that work after the block is filtered again