Some frameworks make a network call look exactly like an ordinary method call on a local object. Explain what that abstraction cannot hide, and contrast it with languages and runtimes that keep remoteness visible in the syntax or the type.
answer
- Waldo: latency, memory, partial failure, concurrency
- RMI/CORBA hide it; Erlang's ! and monitors do not
- Go: context deadline + (value, error)
- Cap'n Proto eventual send = promise pipelining
- colour: async/suspend vs goroutines and virtual threads
basics
~20 sLatency, partial failure, concurrency and the absence of shared memory cannot be hidden. CORBA and Java RMI put them behind an ordinary call; Erlang keeps send, receive and monitors syntactically distinct, Go forces an error return plus a context deadline, Cap'n Proto uses an eventual-send that returns a promise.
solid answer
~50 sWaldo's four irreducibles — latency, memory access, partial failure, concurrency — are what no transparency layer removes. - **Java RMI / CORBA / DCOM**: the remote object implements a normal interface, so a microsecond call becomes milliseconds and failure arrives as an exception callers routinely wrap away. Price: retry, idempotency and timeout have no place in the signature. - **Erlang/Elixir**: `Pid ! msg` is not a function call, replies are received explicitly with an `after` timeout, and partial failure is first class through links, monitors and supervisors. Price: you write correlation and timeouts yourself. - **Go**: every RPC takes a `context.Context` deadline and returns `(value, error)`, so failure and cancellation are in the type. Price: purely conventional — nothing forces you to check `err`. - **Cap'n Proto / the E language**: remote invocation is an eventual send returning a promise, enabling promise pipelining — dependent calls in one round trip, impossible under a blocking local-looking call.
code
erlang · 7 linesRef = erlang:monitor(process, Pid),
Pid ! {self(), Ref, {get, Key}},
receive
{Ref, Reply} -> Reply;
{'DOWN', Ref, _, _, Why} -> {error, Why}
after 500 -> {error, timeout}
end.go deeper
Know that a remote call is slower and can fail in ways a local call cannot, so it should not be written inside a tight loop.
Name the four irreducibles and give one concrete consequence of each, plus one language that keeps the boundary explicit.
Discuss retry and idempotency under lost replies, deadline propagation, and how interface granularity must change when a boundary becomes remote.
Argue the capability angle: transparency costs promise pipelining and forecloses batching, and pick a house rule for how remoteness appears in types across the whole system rather than per service.
## The promise and the four things it cannot keep Distributed-object systems — CORBA, DCOM, Java RMI, and every "just call the service like a bean" framework since — sell one idea: the caller writes `store.get(key)` and does not care whether the object is in this process. Waldo, Wyant, Wollrath and Kendall's 1994 note "A Note on Distributed Computing" enumerated the four differences that survive any such layer. **Latency** changes by four to six orders of magnitude. Code shaped for cheap calls — a loop of accessors, a chatty interface — becomes unshippable, and the fix (batching, coarse-grained calls) is an interface redesign, not a configuration change. **Memory access** is not shared. Local calls pass references and mutate in place; remote calls copy. Aliasing, identity and mutation-visible-to-both stop working, which is why every RPC system grows a serialization model and every serialization model grows an argument about cycles and identity. **Partial failure** has no local analogue. A local call either returns or throws, and the callee's state and the caller's state agree. A remote call can succeed at the callee and be lost on the way back, so a retry is a second execution. Idempotency keys and at-least-once semantics exist because of this one difference; a transparent call site has nowhere to express them. **Concurrency** arrives uninvited: remote objects serve many callers, so invariants that held under single-threaded local use no longer hold. ## Designs that keep remoteness visible **Erlang and Elixir** never offered the illusion. Sending is an operator, `Pid ! Message`, not a call; a reply is an explicit `receive` with an `after Timeout` clause; there is no shared memory to lose because processes never shared any; and partial failure is modelled directly with `link`/`monitor` plus supervisors that decide restart policy at the level that can. Locality is one axis: the same send works to a process on another node. What you give up is convenience — request/response correlation, timeouts and retries are your code, or a library like GenServer's `call`, which reintroduces a blocking wrapper *with an explicit timeout argument*. **Go** keeps the call shape but forces the differences into the signature: `ctx context.Context` carries deadline and cancellation, and `(T, error)` makes failure a value you must handle. The discipline is conventional rather than enforced — `_ = err` compiles — but the reader sees a network boundary in the parameter list. **Cap'n Proto and its ancestor, the E language**, change the calling *semantics*: a remote invocation is an eventual send that returns a promise immediately, and you may invoke methods on that promise before it resolves. The server-side pipeline runs dependent calls in one round trip — `root.getFolder().getFile().read()` costs one network trip, not three. That optimisation is unavailable to any system whose selling point is that a remote call looks like a blocking local one, which is the sharpest argument that transparency is not free convenience but a lost capability. ## Function colour: the same leak inside one process The visibility question recurs without a network. JavaScript, C# and Kotlin mark asynchrony in the signature (`async`, `suspend`), so a leaf function turning remote propagates upward through every caller — the "colour" problem — and blocking on a result can deadlock (classic ASP.NET's captured synchronization context on `.Result`). Go's goroutines and Java's virtual threads deliberately uncolour: blocking is cheap, so ordinary call syntax stays. The tradeoff is exactly the one above — uncoloured code reads uniformly and hides which calls do I/O; coloured code advertises it and forces mechanical churn when a boundary moves. ## What to do as an architect Make the boundary a designed artefact: coarse-grained operations sized for one round trip, explicit deadlines, an explicit failure type, and an idempotency story for retries. Do not let a code generator produce local-looking interfaces and call that an architecture. Where the ecosystem hides remoteness, restore it in the type you expose — return a result type that includes failure, take a deadline parameter — so the caller cannot forget what the network already knows.
- Promise pipelining is often cited as a capability that local-looking RPC gives up. Explain the mechanism.In Cap'n Proto and the E language a remote call returns a promise immediately, and you can call methods on that unresolved promise. Those follow-on calls are shipped to the server, which applies them as soon as the earlier result exists, so a chain of dependent calls costs one round trip. A blocking call that pretends to be local must have the value in hand before the next call can be written, so each dependency costs a trip.
- Go and Java virtual threads remove async colouring. Does that reintroduce the transparency problem?Partly. It restores ordinary call syntax for blocking operations, so the call site no longer advertises that it waits. What it does not restore is the pretence that failure and deadlines are absent: Go still returns an error and takes a context, and virtual-thread code still faces timeouts and partial failure. Colour and remoteness are separate axes, and only the syntactic one is being removed.
saying these in an interview costs you the question
- Claiming a good enough framework can make remote calls indistinguishable from local ones
- Treating a timeout as the whole story and ignoring the lost-reply case that makes retries a second execution
- Assuming exceptions cover partial failure the way they cover local errors, when the callee may have committed
- Calling Erlang's explicit send-and-receive mere boilerplate rather than the failure model being visible