skip to content

Blind Spots and Leaks

Why wrapping silently stops applying: self-calls that never leave the object, unoverridable members, broken identity comparisons, and noisy stack traces. Interviewers use self-invocation as the trap.

on this pageshow

questions

5

A caching wrapper speeds up calls from outside an object but never fires for a call the object makes on itself, why?

level: middleimportance: must knowfreq 66%

answer

  1. interception is a boundary technique
  2. only calls that arrive are seen
  3. which reference carries the inner call
  4. self-reference points at the target
  5. no stand-in in the path, no behaviour

basics

~20 s

Interception only runs where a call crosses from one object into another. A wrapper sees the calls made through the reference it was handed; a call the object makes on itself travels through its own self-reference and never reaches the wrapper.

solid answer

~40 s

A wrapper adds the behaviour on **its own copy** of the member and then forwards to the target, leaving the target's code untouched. Outside callers are handed the wrapper, so their calls land on it first. But when the object invokes one of its own members, the receiver is its internal self-reference, which points at the target; the wrapper is not on that path and never had a chance to run. The bug is silent, because the call still succeeds and only the caching, retry or audit step is missing. It is also path-dependent, so a test that drives the member from outside passes while the production path through the self-call does not.

code

pseudocode · 14 lines
pseudocode
object Report:
    method summary():
        rows = expensive()          // receiver is the object itself
        return format(rows)
    method expensive():
        return slowQuery()

object CachingStandIn(target):
    method summary():   return cacheOrCall("summary", target.summary)
    method expensive(): return cacheOrCall("expensive", target.expensive)

handedOut = CachingStandIn(Report())
handedOut.expensive()   // arrives at the stand-in, cached
handedOut.summary()     // summary cached; the expensive() inside it is not

go deeper

for a junior

Recall that a wrapper is a separate object placed in front of the real one, and that it can only act on calls that are actually sent to it.

for a middle

Explain which reference carries an internal call, why the wrapper is not on that path, and why the symptom is a silent no-op rather than an error.

for a senior

Diagnose it in a running system: show the behaviour applies from outside, trace the path of the failing call, and name the reference it travelled through.

for a principal

Judge whether a mechanism whose failure mode is silence is acceptable for the concern involved, and what evidence you would demand that it actually applied.

## The parts: a target, a stand-in, and the reference in between A **stand-in** (a wrapper, an interceptor) is an object that satisfies the same contract as a **target** object and holds a reference to it. Every member on the stand-in does the same three things: run some added behaviour, forward the call to the target, and run more added behaviour on the way out. Caching, retrying, timing, auditing and permission checks are all built this way, and the appeal is that the target's own source never changes. That appeal is also the source of the blind spot. The added behaviour lives **on the stand-in's copy of the member**, not on the target's. So it runs exactly once per call that *arrives at the stand-in*, and not at all for a call that arrives anywhere else. ## Why the self-call never arrives When code outside the object asks for a collaborator, the wiring hands it the stand-in, so its calls land there first and the behaviour fires. Inside the target the situation is different. When one of the target's members calls another member of the same object, the receiver of that call is the object's **own reference to itself**, and that reference points at the target, because the target is the object the code is executing in. The target was never told that a stand-in exists, and nothing rewrote the call site. So the call passes straight from one of the target's members to another. Interception is a **boundary technique**: it can only act where a call crosses from one object to another, and between two members of the same object there is no boundary to stand on. | call | receiver | added behaviour runs? | |---|---|---| | outside caller through the handed-out reference | the stand-in | yes | | member to another member of the same object | the target itself | no | | outside caller holding a reference obtained before wrapping | the target | no | | member to a collaborator that is itself wrapped | that collaborator's stand-in | yes | The last row matters: an internal call is not doomed by being internal. It is doomed by having the target itself as its receiver. A call the target makes outward, to some other object that is wrapped, is intercepted normally. ## Three properties that make it a favourite interview trap - **It fails silently.** Nothing throws and no log line appears. A cache that is never consulted is indistinguishable from a cache that always misses, and an audit step that never ran leaves exactly the evidence you would expect from a call that never happened. - **It is invisible at the declaration.** The member carries whatever marker requests the behaviour, and that marker says nothing about who is allowed to call it. Reading the member tells you nothing about which of its callers are covered. - **It is path-dependent.** The same member behaves one way through the handed-out reference and another way through the self-reference, so coverage depends on the call graph rather than on the code you are looking at. ## The same rule wearing other faces 1. **A captured callback.** If the target hands one of its own members to something else to invoke later, what was captured is the target, so every later invocation enters the target directly. 2. **A reference that escaped.** Any reference to the raw target obtained before or around the wiring keeps working and keeps bypassing the stand-in. 3. **Construction-time work.** A wrapper is placed around an object that already exists, so whatever the target did while it was being built happened before there was anything to intercept it. All three are the same sentence: only calls whose receiver is the stand-in are intercepted. ## Reading the symptom The diagnosis runs in three steps. First, confirm the behaviour applies at all by exercising the member from outside; if it does not fire there either, the problem is the wiring, not the call path. Second, follow the path the failing call actually took and look for a member of the same object calling another. Third, ask which reference carried the failing call: the handed-out one, or the object's own. A stack trace helps here, because the frames belonging to the interception layers are present on a covered path and absent on a bypassed one. ## What this is not It is not a defect in how the stand-in was produced. Whatever technique built it, a call that does not leave the target cannot be seen. And it is not an ordering problem between several stacked handlers: with a self-call, no handler ran at all, so no rearrangement of them changes anything.

  • Does the same blind spot appear when the target hands one of its own members to something else as a callback?
    Yes. What is captured is the target, not the stand-in, so every later invocation through that callback enters the target directly and the added behaviour is absent. It is the same rule on a different path: the receiver decides whether interception happens, and the receiver there is the raw object.
  • Why does a test that calls the member directly fail to catch this?
    Because the test is an outside caller: it is handed the stand-in, so it takes the covered path. The failing path only exists when the object calls itself, so the test has to drive the outer member that contains the internal call, and assert on the effect of the added behaviour - a second call served from the cache, an audit record - rather than on the return value.

A receptionist who screens every call arriving at the front desk cannot screen the calls staff make to each other from inside the building.

saying these in an interview costs you the question

  • Claims the wrapper also rewrites calls the target makes internally.
  • Says the behaviour lives on the member, so every caller of it is covered.
  • Expects a loud failure when a call bypasses the wrapper.
  • Treats a passing test that calls from outside as proof the path is covered.
  • Blames handler ordering when in fact no handler ran at all.
open as a page

Which calls does a stand-in that intercepts by overriding members silently miss, even when every caller goes through it?

level: middleimportance: must knowfreq 50%

basics

~20 s

Anything that is not a dynamically dispatched call on the instance: members no subtype may replace, members resolved from the declared type or with no receiver at all, direct field reads, and work the target did while it was being constructed.

open as a page

A registry keyed by object identity stops finding an object once it is wrapped for interception, so what broke?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The stand-in is a second object, not the target. Identity comparison, identity-keyed lookups and run-time type tests answer about the object the caller is holding, which is now the wrapper rather than the thing it represents.

open as a page

A wrapped member is also called by the object on itself, so which restructurings make that internal call go through the stand-in?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Make the internal call cross an object boundary. Either move the member onto a separate collaborator the target calls through a reference, or give the target a reference to its own stand-in and have it call through that.

open as a page

Your platform adds cross-cutting behaviour by wrapping, and teams keep shipping paths it silently skips, so what do you standardise?

level: principalimportance: should knowfreq 30%

basics

~20 s

Standardise around the failure mode: wrapping fails silently. Classify each concern by what a silent miss costs, keep the ones that must never be absent off a mechanism that can quietly not apply, and make the guarantee checkable instead of assumed.

open as a page