Which of the four reactive system properties does a service fail to deliver if it uses asynchronous streams internally but calls every dependency with a blocking request?
answer
- the properties describe the boundary
- inside the process is not the boundary
- a waiting caller inherits the callee's tail
- failure unwinds instead of travelling
- more conversations per worker is capacity
basics
~20 sAll four remain unclaimed, because the four properties are claims about the boundary between components and that boundary is still synchronous. Internal streams buy more concurrent conversations per worker, which is a capacity gain, not one of the properties.
solid answer
~50 sThe four properties describe what happens between components, and a blocking call leaves that boundary synchronous however the code inside is written. Because the caller waits for the answer, its own response time is bounded below by the dependency's, so responsiveness under a slow dependency is not protected. Because the failure unwinds into the caller instead of travelling to something that is not failing, it is not contained, so resilience is not there either. Because the caller's committed resources follow the number of in-flight conversations and the dependency's latency, adding replicas of the dependency does not release them, so the elasticity claim is weak. And message-driven, the foundation, is about that boundary and not about push-style values inside one process. What the internal style does buy is holding more conversations per worker - real, useful, and not one of the four.
code
pseudocode · 18 lines// synchronous boundary: the caller is held until the callee answers
function reserveSeat(request):
result = callAndWait(inventoryComponent, request) // pinned here
if result.failed:
fail(request) // callee's failure becomes the caller's
return confirm(result.seat)
// asynchronous boundary: the caller hands the work over and is free
function reserveSeat(request):
send(inventoryComponent, Reserve(request.seat, replyTo = self))
return // nothing held; no answer yet
on message SeatReserved(seat, forRequest):
respond(forRequest, confirm(seat))
on message ReserveFailed(reason, forRequest):
respond(forRequest, degraded(reason)) // failure arrives as data
notify(supervisorOf(inventoryComponent), reason) // recovery is somebody else's jobgo deeper
Take away the distinction: how a component's code is written inside is a different question from what crosses the boundary to another component. The four properties are about the second.
Explain the four things a blocking call transmits - latency as a floor, a held resource, failure as the caller's failure, and load with nothing deciding when it is taken - and why none of them changes with the internal style.
Diagnose it in a running system: make the dependency slow rather than dead, watch the caller's committed capacity and percentile follow it, and compute what the synchronous chain does to the path's availability.
Rule on what the word reactive is allowed to mean in your organisation's design reviews, and require the boundary evidence - held resources, failure routing, effect of replicating a callee - rather than a statement about the libraries in use.
## The properties are claims about the boundary It is easy to read the four system properties as claims about how code is written, because the paradigm and the system claim share a vocabulary. They are not. Responsive, resilient, elastic and message-driven are claims about what happens **between components** - what a component commits while another works, what travels when something breaks, and what a receiving side can decide about the work offered to it. A service whose internals are built from pushed values but whose every outward call is a blocking request has changed the inside and left the boundary exactly as it was. ## What a synchronous call transmits When a component issues a request and waits for the answer, four things cross that boundary whether the team wants them to or not: - **Latency, additively.** The caller's response time cannot be lower than the callee's, so the caller inherits the callee's tail as a floor under its own. - **A resource commitment.** Something on the caller's side is reserved for the duration of the call, so the caller's capacity is tied to the number of conversations in flight multiplied by how long each one lasts. - **Failure, as the caller's failure.** An error unwinds into the caller's own execution rather than arriving somewhere else as data, so the caller must either handle it immediately or fail too. - **Load, unfiltered.** The request enters the callee at the moment it is made. There is no point at which a receiving side decides it will take the next piece of work later. ## Property by property | Property | Delivered by asynchronous internals plus a blocking call? | Why | |---|---|---| | Responsive | not under a slow dependency | the caller's bound sits on top of the callee's, and requests queue behind committed resources | | Resilient | no | a failure that unwinds into the caller is propagated, not contained; availability multiplies along the chain | | Elastic | partly, and weakly | replicas of the callee do not free the caller's committed capacity, which follows conversations in flight | | Message-driven | no | the property names the component boundary, and that boundary still holds a caller until an answer comes back | The honest gain is missing from the table because it is not one of the four: a stream-shaped internal design typically lets one worker hold many more conversations than a design that dedicates one worker per conversation. That is a capacity improvement and worth having. It becomes much smaller when the pipeline's own worker is the thing parked on the blocking call. ## The arithmetic of a synchronous chain Consider a request path that passes through four components, each calling the next and waiting. Two consequences follow directly: 1. **Availability multiplies.** If each hop is independently available 99.9% of the time and the caller has no answer of its own when a callee fails, the path is available about `0.999^4 = 99.6%` of the time. Every synchronous dependency added lowers the product. 2. **The tail accumulates.** The path's response time is at least the sum of the hops', so its high percentile is dominated by whichever hop is worst at that moment - and different hops are worst at different moments. Neither number changes because the code inside each hop was written with stream operators. ## What changing the boundary would look like The shape of the alternative is to hand work over rather than hold it: the sender addresses a component, the answer and the failure both arrive later as messages, and the receiving side controls when it takes more. That has consequences a reviewer can look for - the sender's resources are not held for the duration, a failure can be routed to something that is not itself failing, and the recipient can be replaced or replicated while senders keep sending. It also costs: the conversation now has explicit state, and correlating an answer with the request that caused it is work the call stack used to do for free. That trade is a separate subject; the point here is that it is where the four properties are won or lost. ## How to spot the claim in a review Three questions settle it quickly, and none of them is about the code style: - When this component's dependency takes ten seconds to answer, what is this component doing for those ten seconds, and how much of its capacity is that? - When the dependency fails, who learns about it besides the caller that was waiting, and who is responsible for recovery? - If the dependency is replicated to three times its size, what changes for the caller? If the answers are "waiting", "nobody", and "nothing, until latency drops", the service has a reactive internal style and none of the four system properties.
- Is the internal stream style worthless here, then?No - it typically lets one worker carry far more conversations than a one-worker-per-conversation design, which is a genuine capacity gain. It is just not one of the four properties, and much of it is spent anyway when the pipeline's own worker ends up parked on a blocking call.
- What single symptom would you look for to show the boundary is still synchronous?Make the dependency slow rather than dead and watch the caller. If the caller's committed capacity and its response-time percentile both track the dependency's delay, the boundary is transmitting latency and resource commitment - which is what a synchronous call does, regardless of the code style inside.
- How does a synchronous chain change the availability a caller can offer?It multiplies. With four hops each independently available 99.9% of the time and no answer of its own when a hop fails, the path is available about 99.6% of the time. Each further synchronous dependency lowers the product, which is why containment at the boundary matters more than reliability per component.
saying these in an interview costs you the question
- Calling a service reactive because its code uses stream types
- Believing asynchronous internals make a synchronous boundary asynchronous
- Expecting replicas of a dependency to free a blocked caller
- Ignoring that a waiting caller inherits the callee's tail
- Treating separate processes as failure containment by themselves