Being Reactive Even When Your Upstream Isn't
Modern systems rarely get built in a perfect greenfield. You inherit blocking SDKs, legacy REST services, JDBC drivers, and upstream APIs that have never heard the word “reactive.” Yet you still want to design your own service using Spring WebFlux, R2DBC, and reactive patterns—because your downstream consumers benefit massively from it.
And that’s the key idea of this post:
You can provide a fully reactive experience to downstream services even when your upstream dependencies aren’t reactive at all. You don’t need end-to-end reactivity to gain (and give!) reactive benefits.
What Does “Reactive” Really Mean?
We often talk about “reactive systems” as if every component in the pipeline must be non-blocking for the whole architecture to work. In reality, reactivity is more local and more composable than that.
When we say “my service is reactive”, we mean:
- It handles requests on a non-blocking runtime (e.g., Netty).
- It does not block threads waiting for I/O.
- It exposes reactive APIs (
Mono,Flux) to downstream callers. - It processes workloads efficiently under load due to event-loop and backpressure principles.
None of this requires upstream dependencies to be reactive. It only requires you to avoid blocking in your own execution path.
Calling a Non-Reactive Upstream with WebClient Is Still Reactive
Let’s imagine your upstream service is a classic Spring MVC REST API or even a PHP/Node server. Totally blocking on its side.
Your service calls it using WebClient:
public Mono<Foo> getFoo(String id) {
return webClient.get()
.uri("/foo/{id}", id)
.retrieve()
.bodyToMono(Foo.class);
}
Even though the upstream service itself performs blocking I/O internally, your call is still non-blocking:
- Netty sends the request asynchronously.
- Your event-loop threads are not blocked.
- The returned
Monorepresents a deferred result. - Other requests can be processed concurrently without waiting.
To your downstream services, you remain fully reactive.
The Only Thing That Breaks Reactivity: Blocking on Your Side
Where people fall into trouble is not the upstream service—but their own code.
These patterns kill your reactivity:
Once you block, you lose WebFlux’s scalability benefits. Under load, your app becomes slow, consumes more threads, and may even lock up.
So the rule is simple:
Don’t block. Don’t force asynchronous/reactive types back into synchronous ones.
What If the Upstream Dependency Is Only Available as a Blocking API?
Sometimes the upstream doesn’t even expose HTTP. You may be stuck with:
- JDBC / JPA
- Legacy SOAP clients
- Old Java SDKs that block
You can still keep your reactive contract with downstream callers by isolating the blocking call:
public Mono<Foo> getFoo(String id) {
return Mono.fromCallable(() -> blockingClient.getFoo(id))
.subscribeOn(Schedulers.boundedElastic());
}
Here:
- Your public API stays reactive.
- The blocking operation is moved to a dedicated thread pool.
- Your event-loop threads remain clean and fast.
While this is not “pure” end-to-end reactivity, it preserves the reactive surface area of your service—what downstream consumers see and rely on.
Reactive as a Value You Provide Forward
Downstream services benefit when you stay reactive:
- They get a non-blocking API.
- They can compose your calls with their own reactive pipelines.
- They benefit from backpressure and event-loop efficiency.
- They experience higher throughput and better resource utilization.
Even if your upstream is not reactive, you can still push the ecosystem forward by choosing a reactive foundation. Your service becomes the first domino in encouraging a broader migration.
Just like you can design clean REST APIs even when consuming messy ones, you can design a clean reactive API even when upstream dependencies force compromises internally.
Conclusion
With Spring WebFlux, WebClient, and R2DBC:
- Upstream being non-reactive is not a blocker.
- You can still maintain a reactive contract for all downstream consumers.
- You only need to avoid blocking on your own threads.
- Reactive is still a meaningful architectural improvement—even in isolation.
You give your consumers something better than you received. And over time, that can reshape the entire system landscape.
Happy coding! 💻