skip to content

Manifesto Properties

The four properties a reactive system claims, responsive, resilient, elastic and message-driven, and how they depend on one another. Interviewers want evidence for each, not the slogan.

on this pageshow

questions

5

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?

level: seniorimportance: must knowfreq 55%

answer

  1. the properties describe the boundary
  2. inside the process is not the boundary
  3. a waiting caller inherits the callee's tail
  4. failure unwinds instead of travelling
  5. more conversations per worker is capacity

basics

~20 s

All 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 s

The 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
pseudocode
// 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 job

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

A ticket site's on-sale dashboard shows a healthy mean response time while buyers report waits - what measurement would actually prove the system is responsive?

level: seniorimportance: must knowfreq 62%

basics

~20 s

A high percentile of one named request, measured over every outcome including timeouts and refusals, at a stated arrival rate, across the spike window. Responsiveness is a bounded tail under load, not a healthy average.

open as a page

The Reactive Manifesto names responsive, resilient, elastic and message-driven - how do those four properties depend on one another?

level: middleimportance: should knowfreq 50%

basics

~20 s

Responsiveness is the goal. Resilience and elasticity are the means that keep it true under failure and under changing load. Asynchronous message passing is the foundation both means stand on. They are four layers, not four equal virtues.

open as a page

What experiment during a ticket on-sale would prove a resilience claim - one failing dependency, the rest of the system unaffected - is real?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Inject a slow dependency under the on-sale load and compare two request paths: one that never touches it and one that always does. Containment means the untouched path keeps its response-time bound and the affected path still answers inside one.

open as a page

Elasticity is meant to protect responsiveness, yet scaling out mid on-sale worsened the checkout request's 99th-percentile latency - how would you settle that trade-off?

level: principalimportance: nice to knowfreq 33%

basics

~20 s

Rank the properties before arguing: responsiveness is the goal and elasticity is an instrument judged only by whether the bound holds. State the bound, measure the scale event as a cost against it, and for a scheduled spike provision ahead rather than react.

open as a page