skip to content

When no ambient tenant or actor exists — a scheduled job, a migration — should filtering and stamping fail or write null?

level: principalimportance: nice to knowfreq 38%

answer

  1. no request, no ambient context
  2. tenant and actor deserve opposite defaults
  3. null discriminator matches nothing
  4. name the job as the actor
  5. enforce in the mechanism, not per caller

basics

~20 s

Treat them differently: a missing tenant should fail the unit of work, since filtering on nothing either leaks or silently matches no rows, while a missing actor should resolve to a named synthetic principal rather than null.

solid answer

~50 s

Both the read-side filter and the write-side stamps depend on ambient values, and unattended code — schedulers, importers, message consumers, migrations — has none. The policy has to be explicit, because the defaults are bad in opposite directions. For **tenant**, absence is a correctness and confidentiality question: filtering on a null discriminator matches nothing (a silent empty result that looks like missing data) or, if the code compensates, everything. Failing closed — refuse to run tenant-scoped work without a bound tenant — is the position that fails loudly at the boundary instead of quietly in the data. For **actor**, absence is an accountability question: a null tells the future reader nothing about the writes nobody was watching. Bind a synthetic principal that names the job. The general rule is that the boundary that starts unattended work is responsible for establishing the same ambient context a request would have.

go deeper

for a junior

Know that background work has no logged-in user or request behind it, so the values the filter and the stamps rely on have to come from somewhere else.

for a middle

Explain what actually happens when the value is absent — a discriminator compared to null matches no rows, and an unstamped write leaves an unanswerable audit column.

for a senior

Show that you would establish the same context at every entrypoint and enforce the requirement inside the mechanism, then test a job that runs with nothing bound.

for a principal

Defend two different defaults for two different risks, name the small set of operations allowed to be cross-tenant or unattributed, and say how that set is authorised and kept small.

## Why the question comes up at all Ambient context is established where work enters the system. A request has an authenticated principal and usually a tenant derived from it, so the read-side filter and the write-side stamps both have what they need. Then the system grows the other kind of entrypoint: - a scheduler that runs work on a timer; - a consumer that reacts to a message; - an importer processing a supplied file; - a maintenance task run by an operator; - a schema or data migration executed at deploy time. None of these has a request. Whoever wrote the filter and the stamping listener has already decided, by accident or on purpose, what those paths do — and the accidental decision is almost always null. ## Two problems that look like one They share a cause and deserve opposite answers. | | Missing tenant | Missing actor | |---|---|---| | Nature of the failure | Correctness and confidentiality | Accountability | | Symptom of writing null | Reads match nothing, or the code compensates and matches everything | Rows whose history says nobody changed them | | When it is discovered | On a support call, or by the affected customer | During an incident review, when it is too late to reconstruct | | Reasonable default | Fail the unit of work | Bind a named synthetic principal | | Escape hatch | An explicit "run as tenant X" or a deliberately cross-tenant operation | None needed — every job can be named | The asymmetry is the point of the question. A missing tenant means the system does not know **whose data this is**, and there is no safe guess. A missing actor means the system does not know **who did this**, and there is a perfectly good answer: the job did. ## The case for failing closed on tenant 1. **Filtering on nothing is not filtering.** A predicate comparing a discriminator to a null value matches no rows under standard SQL comparison semantics, so the job quietly does nothing and reports success. The second-order failure is worse: someone notices and "fixes" it by skipping the filter when no tenant is bound, which turns every unattended path into a cross-tenant path. 2. **The failure is loud and early.** Refusing at the boundary produces a stack of work that did not run, which is noticed. A leak produces data that did run, which is not. 3. **The fix is cheap and explicit.** Unattended work that legitimately concerns one tenant should bind that tenant — iterating tenants and running the job once per tenant with the context bound is the ordinary shape. Work that legitimately spans tenants should say so, by name, in one place. ## The case against null actors An audit column exists to answer a question later, under pressure, about a change nobody remembers. The writes with no interactive user behind them are precisely the ones that will be hardest to reconstruct: a nightly correction, an importer with a bad file, a retried consumer. A null in that column converts "the importer did it at 03:14" into "unknown". A synthetic principal costs almost nothing: a stable identifier naming the job, bound by the same mechanism a request principal uses, so the stamping listener needs no special case. It also makes the audit trail queryable in the way that matters — you can ask how many rows a given job touched. There is a stricter position worth acknowledging: in some regulated settings, no write may be unattributable to a real accountable identity, and unattended writes are either forbidden or attributed to the operator who scheduled them. That is a legitimate stance, and it is a policy decision rather than a technical one. ## Making the policy hold - **Establish context at every entrypoint, not just the request one.** Whatever binds the principal and tenant for a request should be invoked by the scheduler, the consumer and the task runner too. If that is awkward, the ambient mechanism is coupled to the request path and should be loosened. - **Fail in the mechanism, not in each caller.** The listener or filter should raise when a required value is absent. A convention that every job remembers to bind context is the same convention that failed for query predicates. - **Make the exception explicit and few.** A small, named set of operations may run cross-tenant or unattributed; everything else must not. - **Test the unattended paths.** A test that runs a job with no context bound and asserts the expected outcome — a refusal, or a stamp naming the job — is the only thing that keeps the policy from decaying. - **Watch migrations separately.** Deploy-time data changes usually run outside the mapping altogether, so they neither filter nor stamp; if they must, the statement has to set the columns itself. ## What a strong answer sounds like Not "we use the system user for everything", and not "we throw if anything is missing". A strong answer separates the two concerns, defends failing closed where the wrong guess leaks data, defends naming the actor where the wrong guess erases accountability, and puts both decisions in the mechanism rather than in the discipline of whoever writes the next job.

  • What is wrong with skipping the tenant filter when no tenant is bound?
    It converts every unattended path into a cross-tenant one, and it does so in the code that handles the case nobody tested. The rule inverts exactly where the consequence is worst: the paths with no principal behind them become the paths with no restriction. Refusing the work, or requiring an explicit cross-tenant operation, keeps the exception visible.
  • How should a job that legitimately processes every tenant be structured?
    Enumerate the tenants in an explicitly cross-tenant step, then run the per-tenant work with that tenant bound for each unit of work. The filter and the stamps then behave exactly as they do for a request, and the only privileged code is the short enumeration, which can be reviewed and authorised on its own.
  • Do deploy-time data migrations fit this policy?
    Usually not, because they run outside the mapping and so neither the filter nor the stamping listener is involved. Their statements must therefore set audit columns themselves if the columns are meant to be meaningful, and they must carry any tenant predicate explicitly. Treat them as a known unfiltered, unstamped path rather than an oversight.

saying these in an interview costs you the question

  • Writes null when no actor is available and calls it harmless
  • Skips the tenant predicate whenever no tenant is bound
  • Assumes a null discriminator comparison matches every row
  • Relies on each job remembering to bind context
  • Uses one shared administrative identity for every background writer
  • Never tests a code path with no ambient context bound