skip to content

A scheduled job and a retried queue message run with no charterer bound in the request context — what must happen, and why is 'all charterers' the dangerous default?

level: seniorimportance: should knowfreq 46%

answer

  1. no request, no binding
  2. absent must not mean all
  3. the read raises, not returns wildcard
  4. bind per message, clear between
  5. return the connection clean

basics

~20 s

Unbound must mean refuse, not proceed. A data-access layer that drops the charterer predicate when nothing is bound returns every charterer's rows, so make the read raise; background work binds a charterer explicitly, per message, and clears it afterwards.

solid answer

~40 s

Every guarantee about the charterer was erected by a request, and these paths have none. The failure mode is specific: a helper that appends `AND charterer_id = :bound` simply omits the clause when nothing is bound, and an absent filter is not *no rows* — it is *all rows*. So make the read of the binding raise rather than return a wildcard, which makes a charterer-scoped query unbuildable unbound. Background work then binds deliberately: a consumer takes the charterer from the message envelope and binds it **per message**, clearing between messages so a pooled worker thread cannot carry one into the next; a scheduled job enumerates charterers and binds each in turn. A pooled database connection gets the same discipline — set inside the unit of work, cleared on return.

code

pseudocode · 19 lines
pseudocode
# the read refuses, so a charterer-scoped query cannot be built unbound
function boundCharterer():
    value = context.get("charterer")
    if value is ABSENT:
        raise NoChartererBound("charterer-scoped work ran without a binding")
    return value

# a consumer binds PER MESSAGE and clears between them
function consume(batch):
    for message in batch:
        charterer = message.envelope.charterer
        if charterer is ABSENT:
            park(message, "no attributable charterer")   # not processed under a default
            continue
        context.bind("charterer", charterer)
        try:
            handle(message)
        finally:
            context.clear("charterer")   # the pooled worker thread must not carry it on

go deeper

for a junior

Remember that background code runs with no request behind it, so nothing has bound a charterer for it. Know that a missing filter returns everything rather than nothing, which is why the unbound case has to refuse.

for a middle

Explain the mechanics: where the charterer rides on a message, why binding happens per message rather than per batch, why the clear belongs in a finally block, and why the read itself must raise rather than return a wildcard.

for a senior

Show that you have operated this. The mixed batch bound once, the pooled worker thread that carried a charterer into the next message, the dead-letter replay that arrived without its envelope metadata, the connection returned to the pool still set.

for a principal

Own the invariant across every entry point the system will ever grow. Make the unbound case unbuildable rather than discouraged, require attributable work at the queue boundary, and get the property asserted once so paths written next year inherit it.

Everything that makes charterer isolation work in a request handler is scaffolding erected by the request: the credential was verified, the charterer was derived, the value was bound. Background code inherits none of it and runs with the same data-access helpers. That is where the cross-charterer read actually comes from in a mature service — not from the endpoint everyone reviewed, but from the nightly sweep nobody thought of as a caller. ## The three paths that arrive empty 1. **Scheduled work.** A nightly job that expires stale booking holds, recomputes a vessel's utilisation, or emails a summary. It was started by a clock, and a clock has no charterer. 2. **Queue consumers.** A request enqueued `confirm-booking` and returned. The consumer runs minutes later, possibly on another host, in a process that never saw the original request. 3. **Retries and replays.** The most dangerous of the three, because the message *did* once have a charterer. A retry from a dead-letter queue, or a replay after an incident, re-enters the system through a path that may or may not reconstruct the envelope faithfully. Add to these the smaller cases with the same shape: an operator command run from a terminal, a data-repair script, a start-up warm-up that touches the cache. ## Why the default matters more than the paths The reason this is a design question rather than a checklist is the asymmetry in how a missing value behaves. Consider the helper every service grows: > Append `AND charterer_id = <bound charterer>` to each charterer-scoped query. When nothing is bound, the natural implementations all fail the same way. Appending an empty string drops the clause. Interpolating a null produces a comparison that matches nothing *or* is optimised away, depending on the layer. Passing a wildcard matches everything by construction. **The absent case and the all case end up spelled identically**, and the query that was supposed to be scoped now returns the corpus. So the enforcement has to sit in the read, not in the caller's good intentions: reading the bound charterer when none is bound raises, and the charterer-scoped query is therefore unbuildable rather than merely wrong. This is the same argument as refusing in the derivation path, applied to the code that has no derivation path at all. Note which component is doing what here — this is the data-access layer refusing to construct a query, not an authorization verdict about a principal. There is no principal. ## What background work does instead - **Carry the charterer on the message envelope**, not in the payload a handler happens to read. It is metadata of the work, on a par with a correlation identifier, and it must survive a retry and a dead-letter replay intact. - **Bind per message, never per batch.** A consumer that pulls ten messages and binds once has bound the first message's charterer for all ten. Batches are mixed by default; assume they are. - **Clear between messages.** Worker threads are pooled exactly like request threads, and a binding left behind is the next message's charterer. This fault has a characteristic signature: it is load-dependent, invisible at low throughput, and does not reproduce in a single-message test. - **Iterate explicitly in scheduled work.** A nightly sweep enumerates charterers and binds each in turn, doing one charterer's work inside one binding. `Do it for everyone at once` is precisely the unbounded query you have spent the whole design forbidding. - **Treat a message whose charterer cannot be established as poison.** Park it; do not process it under a default. A message with no attributable charterer is not a message about everyone. ## The pooled connection, on the application side If the charterer is communicated to the data store as a setting on the connection rather than as a parameter, the application owes that setting a life cycle: set it inside the unit of work and clear it before the connection goes back to the pool. A connection returned dirty hands the previous borrower's charterer to the next one — the same class of bug as the uncleared thread binding, with the same load-dependence and the same absence from local testing. The mechanics on the data-store side, and the argument for making isolation structural rather than a filter you remember, belong to the data store's own design; what the application owns is that the value is set and unset around the work, every time, including on the error path. ## How this is found before a customer finds it The cheap, high-value property to assert is negative: call a charterer-scoped data-access helper with nothing bound and require it to raise. That one assertion covers every path that will ever forget, including paths written next year. Pair it with the charterer as a field on every background log event — a job that ran bound says so, and a job that ran unbound becomes visible as an absence rather than as a customer report.

  • A consumer pulls a batch of ten messages and binds the charterer once before the loop. What does that cost you?
    Nine messages processed under the first message's charterer. Batches are mixed by default, so every write in that batch lands against the wrong owner and every read in it returns the wrong rows — all while each individual query looks correctly filtered. Bind inside the loop, clear in a finally block, and let the batch boundary carry no charterer at all.
  • A message arrives from a dead-letter replay with no charterer on its envelope. What do you do with it?
    Park it and alert; do not process it under a default or under the charterer of whatever replayed it. A message with no attributable charterer is not a message about everyone, and guessing turns an infrastructure fault into a data fault. Then fix the replay path, because losing envelope metadata on retry will keep happening and is a property of the transport, not of that one message.
  • Why does a binding left behind on a pooled worker thread so rarely show up in testing?
    Because it needs a second piece of work to land on the same thread, which requires concurrency the test suite does not generate. A single-message test always passes: there is nothing after it to inherit the stale value. The fault is load-dependent, appears first in production, and looks like a cross-charterer read with no bad query anywhere in the code.
  • What single assertion catches the widest range of these faults?
    Call a charterer-scoped data-access helper with nothing bound and require it to raise. That one negative test covers every present and future path that forgets to bind, because it checks the property rather than enumerating callers. Add the charterer as a field on every background log event so a job that ran unbound is visible as an absence rather than as a customer report.

A night-shift crane operator finds a container with no consignee label. Stopping and parking it is correct; deciding it can therefore go on any vessel is how it ends up at the wrong port.

saying these in an interview costs you the question

  • Lets a missing charterer drop the predicate, which returns every charterer's rows
  • Binds once per batch and assumes every message shares a charterer
  • Leaves the binding on a pooled worker thread after the message is handled
  • Processes an unattributable replayed message under a default charterer
  • Returns a database connection to the pool with the charterer still set on it
  • Assumes background code is safe because the endpoints were reviewed