As an architect, how do you choose among the four RSocket interaction models, and what delivery/error guarantees does each imply — especially fire-and-forget?
answer
- cardinality picks model; guarantees veto
- fire-and-forget = at-most-once, no error channel
- send() completes on write, not delivery
- streaming models = REQUEST_N backpressure
- model is part of the wire contract — both peers agree
basics
~20 sMatch the model to the message shape: one reply → request-response; no reply, best-effort → fire-and-forget; server pushes many → request-stream; both sides stream → request-channel. Fire-and-forget gives no app-level acknowledgement or error back, so use it only for tolerable-loss signals.
solid answer
~50 sChoose by cardinality and guarantees. Request-response: exactly one reply, so you get success/error feedback and can retry idempotently — RPC-style calls. Fire-and-forget: no reply frame at all, so the client learns nothing about processing success or failure; send() completes when the frame is written, not delivered. Use it only where loss is acceptable (metrics, telemetry, hints) or where durability is guaranteed elsewhere. Request-stream: one request, a backpressured server stream — subscriptions, tailing, server push. Request-channel: bidirectional backpressured streams for interactive sessions and flow-controlled uploads. Cross-cutting concerns: all streaming models honor Reactive Streams backpressure via REQUEST_N; errors propagate as onError to the requesting side except fire-and-forget which has no error channel. Consider connection multiplexing (many interactions share one connection), resumability/leasing at the protocol level, and that model choice is part of the API contract both peers must agree on.
code
java · 14 lines// Guarantee-driven choice:
// Must know the result -> request-response (observable success/error)
Mono<PaymentResult> pay(PaymentCommand cmd) {
return requester.route("payments.charge")
.data(cmd)
.retrieveMono(PaymentResult.class) // errors surface as onError
.timeout(Duration.ofSeconds(3))
.retryWhen(Retry.backoff(2, Duration.ofMillis(200))); // idempotent only
}
// Loss acceptable -> fire-and-forget (no ack, no error back)
Mono<Void> heartbeat(Ping p) {
return requester.route("presence.ping").data(p).send();
}go deeper
Map each model to its message shape and know fire-and-forget expects no reply.
Explain that only reply-bearing models give success/error feedback; fire-and-forget is best-effort.
Reason about backpressure, retries/idempotency, and observing fire-and-forget outcomes on the responder side.
Treat model choice as a contract/guarantee decision; weigh multiplexing, resumption/leasing, and security/observability tradeoffs.
Choosing an RSocket interaction model is an **API design decision** with real consequences for delivery guarantees, resource use, and error handling. The four models map to message cardinality, but the deeper distinctions are about *what feedback the caller gets*. **The decision matrix (cardinality → model):** - **One request, one response** → **request-response**. HTTP-like RPC. You get a `Mono<T>` with success or error, enabling retries (make them idempotent), timeouts, and circuit breaking. Default choice for command/query with a result. - **One request, no response** → **fire-and-forget**. Use for high-volume, loss-tolerant one-way signals. - **One request, many responses** → **request-stream**. Server push/subscriptions, event tailing, live feeds. Backpressured. - **Many requests, many responses** → **request-channel**. Interactive bidirectional sessions, flow-controlled uploads, adaptive streams. Backpressured both ways. **Delivery & error guarantees — the crux.** - **Request-response** and the streaming models have a **reply channel**, so application errors surface to the caller as `onError` (RSocket error frames), and completion is observable. You can build retries, deadlines, and fallbacks on top. - **Fire-and-forget has *no* reply channel at all.** The client's `send()` returns a `Mono<Void>` that completes when the **frame is written to the transport**, *not* when the server received or processed it. There is **no application acknowledgement and no error propagation** back to the sender. Implications: - Treat it as **best-effort / at-most-once** from the app's perspective. A crashed responder, a decoding failure, or business-rule rejection is invisible to the client. - Only use it where **loss is acceptable** (metrics, telemetry, cache-warm hints, presence pings) or where durability/exactly-once is provided by a *different* mechanism (e.g., the responder immediately persists to a durable log and that log's guarantees govern). - Do **not** use fire-and-forget for commands whose success the caller must know about — that's request-response's job. **Cross-cutting protocol concerns an architect weighs:** - **Backpressure** — all streaming models implement Reactive Streams demand over **REQUEST_N** frames, preventing unbounded buffering; design producers to be lazy/pull-driven, avoid blocking that defeats it. - **Multiplexing** — a single RSocket connection carries many concurrent interactions; you don't open a connection per call. This changes capacity planning versus HTTP/1. - **Resumption & leasing** — RSocket supports session resumption and lease-based flow control at the protocol level; relevant for mobile/lossy networks and load-shedding, though Spring exposes these via lower-level configuration. - **Contract symmetry** — the interaction model is part of the wire contract: both the `@MessageMapping` handler signature and the `RSocketRequester` terminal call must agree. A mismatch (e.g., client `retrieveMono` against a streaming handler) yields degraded/incorrect behavior. Version and document routes and their models like any API. - **Security** — connection-level auth via `@ConnectMapping`/setup metadata plus per-request auth via request metadata; model choice interacts with this (fire-and-forget still carries metadata, but you get no auth-failure feedback to the sender). - **Observability** — because fire-and-forget gives no feedback, instrument the *responder* side (metrics, dead-letter/log) since the client can't observe outcomes. **Anti-patterns.** - Fire-and-forget for critical commands (silent loss). - Request-response used to poll for a stream (chattiness) where request-stream fits. - Opening parallel request-streams to fake bidirectionality instead of using request-channel. - Ignoring backpressure by eagerly buffering, negating RSocket's core benefit. **Bottom line.** Pick the model from message cardinality, but let **required guarantees** veto: if the caller must know the outcome, it cannot be fire-and-forget. Document the chosen model per route as part of the contract, and lean on multiplexing + backpressure as first-class capacity/reliability tools.
- A teammate wants fire-and-forget for order submission to reduce latency. What's your objection?Fire-and-forget gives no application acknowledgement or error back — send() completes on frame write, not processing. Order loss/rejection would be invisible to the client. Use request-response (or persist-then-ack) for anything whose outcome must be known.
- How does connection multiplexing change capacity planning versus HTTP/1?One RSocket connection carries many concurrent interactions, so you size around streams/demand and event-loop capacity rather than connection-per-request pools; fewer handshakes and connections for the same throughput.
saying these in an interview costs you the question
- Using fire-and-forget for operations whose success the caller must confirm
- Believing send()'s completion means the server processed the message
- Thinking each request needs its own connection instead of multiplexing
- Selecting a model purely by cardinality while ignoring required delivery guarantees