REST vs gRPC vs async messaging, request-response vs fire-and-forget vs pub-sub, Feign vs RestTemplate vs WebClient, REST vs SOAP, and Reactor's Mono/Flux with backpressure.
Published September 23, 2026
Simplest to build, test, and reason about — a caller makes an HTTP request and gets a response inline. The direct cost: the caller's own response time is coupled to the callee's availability and latency — if the downstream service is slow or down, the caller is blocked (or fails) waiting on it, and that coupling can cascade across a chain of synchronous calls.
A binary protocol (Protocol Buffers over HTTP/2) with strongly-typed contracts defined in .proto files — client and server code is generated from the same contract, eliminating an entire class of "the client's assumed shape doesn't match the server's actual response" bugs that loosely-typed JSON-over-REST is prone to. Binary encoding plus HTTP/2 multiplexing gives meaningfully lower latency and bandwidth than REST/JSON for high-throughput internal service-to-service traffic — the tradeoff is less human-readability (you can't just curl and eyeball a response the way you can with JSON) and a steeper adoption cost (code generation tooling, less universal client support than plain HTTP/JSON).
[Producer] → publishes an event → [Message Broker: Kafka/RabbitMQ] → [Consumer(s)] process whenever ready
Decouples timing entirely — the producer doesn't wait for (or even know about) the consumer's processing, and a temporarily slow or down consumer doesn't block the producer at all, just delays when the message gets processed. This directly improves resilience to downstream slowness, at the cost of the caller no longer getting an immediate answer — appropriate specifically when the caller doesn't need one.
The deciding question: does the caller need an immediate answer, or can the result be eventual? A checkout flow's "charge the card" step likely needs a synchronous answer (the user is waiting to know if payment succeeded). A checkout flow's "send a confirmation email" step doesn't — that's a natural fit for fire-and-forget async messaging, decoupling email-sending latency/failures entirely from the checkout response the user actually waits on.
A realistic e-commerce checkout: synchronous REST/gRPC for user-facing reads and the payment-charge step (the user is actively waiting), asynchronous messaging for background processing (inventory reservation confirmation, sending notifications, updating analytics) that doesn't block the user-visible response. Treating this as an either/or architectural choice for an entire system is a common design mistake — the right granularity is per-interaction, not per-system.
Mono/Flux (see below) instead of blocking the calling thread — the modern default choice, especially in a service that itself needs to stay non-blocking end-to-end.REST is an architectural style over plain HTTP using standard verbs (GET/POST/PUT/DELETE) and typically JSON — lightweight, stateless, minimal tooling required. SOAP is a stricter, XML-based protocol with a formal contract (WSDL) and built-in standards for retry/security (the WS-* family) — heavier, but still common in enterprise and regulated integrations (banking, insurance, government systems) where SOAP's stronger built-in contracts and standardized security extensions are established requirements, not a stylistic preference.
Mono<User> user = webClient.get().uri("/users/{id}", id).retrieve().bodyToMono(User.class); // 0-or-1 result
Flux<Order> orders = webClient.get().uri("/orders").retrieve().bodyToFlux(Order.class); // 0-to-N results, streamed
Mono represents an asynchronous stream of 0 or 1 results; Flux represents 0 to N. Both carry built-in backpressure — a subscriber can signal how much data it's actually ready to consume, rather than a fast producer overwhelming a slower consumer's buffer, which is the core problem reactive streams (the specification Reactor implements) exist to solve. The practical payoff: a service can handle many concurrent I/O-bound requests on a small, fixed thread pool (no thread blocked per in-flight request the way a traditional synchronous servlet model requires), similar in spirit to virtual threads' I/O-bound scalability benefit but achieved through a fundamentally different mechanism (non-blocking callbacks and operator composition, not lightweight thread scheduling).
Q: If virtual threads make blocking-style code perform like async code, does reactive programming (WebClient/Mono/Flux) still matter? A: For many I/O-bound use cases, virtual threads (see Virtual Threads) do reduce the case for reactive code specifically for concurrency scaling — but Reactor's operator composition (retry, timeout, backpressure, combining multiple streams) offers expressive tools beyond just "don't block a thread," and existing WebFlux-based systems have substantial reasons to stay on the model they're already built around rather than a wholesale rewrite.
Q: When would gRPC be a poor choice despite its performance advantages? A: For a public-facing API consumed by arbitrary third-party clients (browsers, varied external tooling) where REST/JSON's universal support and human-debuggability matter more than internal service-to-service latency — gRPC's tooling and browser support (without a proxy layer like grpc-web) make it a much better fit for internal service-to-service traffic than for a public API surface.
Q: How does a request-response pattern work over an inherently asynchronous message broker? A: Via a correlation ID: the requester publishes a request message tagged with a unique ID and a reply-to destination, then asynchronously waits (or polls) for a response message carrying the same correlation ID on that destination — functionally request-response, but built on top of fundamentally async infrastructure rather than a direct synchronous call.
Q: Why might a team choose Feign over WebClient for simple service-to-service REST calls despite WebClient being the more 'modern' choice? A: If the calling service isn't itself reactive/non-blocking internally, Feign's simpler, more concise declarative style avoids introducing Mono/Flux-based reactive code into an otherwise traditional blocking codebase — reserving WebClient specifically for services already built on the reactive stack end-to-end avoids mixing two different concurrency models unnecessarily.