Compare a protection proxy and a remote proxy: what does each control, and what specific hazards does each introduce?
answer
- Protection = who; Remote = where
- Bypass kills protection proxies (self-invocation!)
- Deny by default; test the deny path
- Remote: latency, by-value args, partial failure
- Timeout + idempotency + circuit breaker
basics
~20 sA protection proxy controls who may call — it checks the caller's rights and forwards or denies. A remote proxy controls where the object is — it hides that the real object lives in another process or machine. The first can be bypassed if callers reach the subject directly; the second hides latency and partial failure.
solid answer
~60 s**Protection proxy:** wraps the subject with an authorization check and denies unauthorized calls. Its hazards are all about being *the* chokepoint — if any path reaches the real subject without going through the proxy (direct instantiation, internal self-calls, another module holding the concrete reference, reflection), the control silently disappears. It must also fail closed on an unknown principal, avoid leaking existence through distinguishable errors, and be tested for its *deny* paths, not just its allow paths. Because it can throw where the subject wouldn't, the interface contract must declare that outcome (an LSP consideration). **Remote proxy:** marshals the call, sends it over a transport, and unmarshals the reply, so the client's code looks local. Its hazards come from that very transparency — the fallacies of distributed computing. Latency is orders of magnitude higher, so chatty fine-grained interfaces built for local calls perform terribly; arguments must be serializable and are passed by *value*, so mutation semantics change; and failures are partial and ambiguous (a timeout doesn't tell you whether the call executed), so operations should be idempotent and the proxy needs timeouts, retries with backoff, and a circuit breaker rather than pretending the call is local.
go deeper
Define both: one checks permissions before forwarding, the other hides that the object is on another machine. Name one risk each (bypass; network failure).
Add mechanism detail — where the check lives, what marshalling means — and the core mitigations: deny by default, timeouts, retries.
Discuss self-invocation bypass, LSP tension when a proxy throws, coarse vs chatty remote interfaces, idempotency under ambiguous timeouts, and circuit breakers/bulkheads.
Position them as policy and topology decisions: where the policy decision point lives, why remoteness must stay visible in the contract, and how sidecars/gateways are the same pattern at infrastructure scope with their own operational cost.
Both are Proxy variants — the surrogate controls access — but they control different dimensions, and the engineering consequences barely overlap. --- ## Protection proxy — controlling *who* ### Mechanism The proxy implements the subject's interface, inspects the caller's identity/roles/permissions (from an ambient security context, an explicit parameter, or a token), and either forwards or denies. The point is that the authorization rule lives in one place, and the domain object stays free of security code. ``` class SecureAccounts implements Accounts { Accounts real; Principals principals; Balance balanceOf(id) { if (!principals.current().canView(id)) throw new AccessDenied(); return real.balanceOf(id); } } ``` ### Hazards 1. **Bypass is total and silent.** The control only exists if *every* path goes through the proxy. Direct construction of the real subject, a second module holding the concrete type, deserialization, reflection, or a debug endpoint all defeat it. Mitigation: make the real subject non-public/package-private, hand out only the proxy from the factory/container, and add an architecture test that forbids depending on the concrete type. 2. **Self-invocation.** With framework-generated proxies, a method the real object calls on *itself* goes through the internal reference, not the proxy — so the check is skipped. This is the single most common real-world security surprise with proxy-based interception. 3. **Fail-open defaults.** If the principal is unknown/absent, the safe outcome is *deny*. Code shaped like `if (principal != null && !allowed) throw` fails open when the principal is null. 4. **Granularity mismatch.** Method-level checks can't express "you may read *these* rows". Coarse proxies push you into fetching data then filtering it — a leak if the filtering is forgotten, and a performance problem regardless. Row-level rules usually belong closer to the query. 5. **Information leakage.** Distinguishing "not found" from "forbidden" tells an attacker the resource exists. Decide deliberately per resource. 6. **Testing bias.** Teams test the allow path. The deny path is the security control; it needs tests, including for each new method added to the interface (a newly added method that the proxy forgot to guard is a classic hole — an abstract/complete implementation or interception-by-default avoids it). 7. **LSP tension.** The proxy throws where the subject would succeed. That's acceptable only if the *interface contract* documents authorization failure as a legal outcome; otherwise clients written against the subject break. --- ## Remote proxy — controlling *where* ### Mechanism Also called a *stub* or *ambassador*. It implements the subject interface locally, serializes the method identity and arguments, sends them via a transport (HTTP, gRPC, message queue, custom RPC), waits, deserializes the response, and returns it — or translates transport errors into exceptions. ### Hazards — the fallacies of distributed computing 1. **Latency is not zero.** A local call is nanoseconds; a same-datacenter round trip is ~0.5–1 ms; cross-region tens of ms. An interface designed for local use (many small getters) becomes hundreds of round trips. Remote interfaces should be *coarse-grained* — send one document, not twelve setters. This is the well-known lesson from early distributed-object frameworks that tried to make remote calls perfectly transparent. 2. **Parameters change semantics.** Arguments are serialized and passed **by value**; the callee mutating them doesn't affect the caller's copy. Object graphs may be deep, cyclic, or contain non-serializable handles. Callbacks require a reverse channel. 3. **Partial failure and ambiguity.** A timeout means "I don't know": the call may have succeeded, failed, or still be running. Therefore: make operations **idempotent** (idempotency keys), and only retry when it is safe. Blind retries on non-idempotent operations double-charge customers. 4. **The network is not reliable or secure.** You need timeouts on every call (a missing timeout is the classic thread-pool exhaustion cascade), retries with exponential backoff and jitter, a circuit breaker/bulkhead so a slow dependency doesn't take you down, plus transport encryption and authentication. 5. **Versioning.** Both sides evolve independently; the proxy's serialization contract becomes a compatibility boundary requiring additive, tolerant schema changes. 6. **Observability.** Propagate trace/correlation IDs across the hop; without them a latency regression is unattributable. 7. **Transparency is the danger.** The syntactic transparency that makes a *local* proxy elegant makes a *remote* proxy misleading. Modern practice keeps the call shape convenient but makes remoteness visible in the type/contract — async return types, explicit timeouts, declared failure modes — so callers can't mistake it for a field access. --- ## Side by side | | Protection proxy | Remote proxy | |---|---|---| | Controls | Who may call | Where the object lives | | Fails by | Denying (deterministically) | Timing out, partially failing (nondeterministically) | | Main risk | Being bypassed | Being believed (looks local) | | Key mitigations | Single chokepoint, deny-by-default, deny-path tests, watch self-invocation | Coarse interfaces, timeouts, idempotency, retries+backoff, circuit breaker, tracing | | Typical modern form | Framework security interceptor / policy decision point | RPC client stub, sidecar, API gateway | ## They compose A typical service call passes through several proxies: a client-side remote stub, a sidecar doing mTLS and retries, a gateway doing rate limiting, and a server-side protection proxy doing authorization. Each is the same pattern applied at a different scope — which is why "proxy" names both a class-level GoF pattern and a piece of network infrastructure.
- Why is a timeout non-negotiable in a remote proxy?Without one, a hung dependency holds the caller's thread/connection indefinitely; under load every worker ends up blocked on it and the caller fails too. A bounded timeout converts an unbounded hang into a fast, handleable error — and pairs with a circuit breaker so you stop calling a dependency that's already failing.
- A framework-generated protection proxy guards `transfer()`, but an internal call from `batchTransfer()` in the same class skips the check. Why?Self-invocation: the object calls its own method through its internal reference, which is the real subject, not the proxy. Fixes are to route through the proxied reference (inject self, or an application-context lookup), split the methods into separate collaborators, or use weaving that instruments the class itself rather than wrapping it.
- When should you deliberately *not* make a remote call look like a local one?Almost always at the API design level: expose asynchronous/awaitable results, explicit timeouts, and distinct failure types so callers reason about latency and partial failure. Syntactic convenience is fine; semantic disguise is not.
A protection proxy is a door guard: useless if there's an unlocked side entrance. A remote proxy is a translator on a phone line to another country: the conversation looks normal, but there's delay, the line can drop mid-sentence, and you can't tell whether your last request was heard.
saying these in an interview costs you the question
- "The proxy makes remote calls just like local calls" — treated as a benefit rather than the classic distributed-computing fallacy
- Retrying a non-idempotent remote operation after a timeout
- Authorization checks that fail open when the principal is missing
- Assuming a protection proxy secures the object even when other code can construct or reference the real subject directly
- Forgetting that a self-call inside the real object bypasses a generated proxy
- Fine-grained chatty interfaces reused unchanged across a network boundary