Execute blocking JDBC call in Spring Webflux
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Spring WebFlux is built around non-blocking request handling, while JDBC is fundamentally blocking. That mismatch is the core issue: if you call JDBC directly on a Netty event-loop thread, one slow query can stall request processing for unrelated clients.
The Rule: Never Block the Event Loop
In WebFlux, request handling often starts on a small set of event-loop threads. Those threads are designed to hand work off quickly, not to sit idle waiting for a database round trip.
This is the anti-pattern:
The method returns Mono<User>, but the JDBC call already blocked before the Mono was created. That means the code looks reactive while behaving synchronously.
Offload Blocking JDBC Work Explicitly
If you must keep JDBC, wrap the blocking work in Mono.fromCallable or Flux.defer and schedule it on Schedulers.boundedElastic().
boundedElastic exists specifically for blocking work such as file I/O, JDBC, or legacy SDK calls. It prevents that work from consuming the event-loop threads.
Keep the Blocking Boundary in the Service Layer
A clean architecture makes the blocking boundary obvious. Controllers stay reactive, while the service encapsulates the blocking database call.
This is easier to reason about than letting controllers sprinkle scheduler logic around each query.
Prefer R2DBC When the Whole Stack Should Be Reactive
Wrapping JDBC in boundedElastic is a compatibility strategy, not a fully reactive data layer. If the application is genuinely meant to be reactive end to end, R2DBC is the better fit because it uses non-blocking database drivers.
Use wrapped JDBC when these conditions are true:
- you are migrating incrementally from Spring MVC to WebFlux
- a legacy library requires JDBC
- rewriting the persistence layer is not yet practical
Use R2DBC when the goal is a consistently reactive stack with fewer hidden blocking points.
Be Honest About Throughput Limits
Moving JDBC to boundedElastic avoids blocking the event loop, but it does not make the database call non-blocking. Queries still occupy threads while waiting on the database.
That means throughput is now limited by:
- the bounded elastic scheduler capacity
- the JDBC connection pool
- database latency
If load increases, the system can still saturate. The architecture is safer than blocking the event loop, but it is not magical.
Handling Transactions and Errors
When you keep JDBC, use the normal Spring transaction model for that blocking code path. Do not expect reactive transaction semantics to appear automatically just because the return type is Mono.
Error handling also belongs in the reactive chain:
That keeps failure handling explicit instead of letting checked and unchecked exceptions leak unpredictably across layers.
Avoid Mixing Reactive Signatures With Hidden Blocking
One of the worst outcomes is a codebase that returns Mono and Flux everywhere while doing blocking work directly under the surface. That gives you the complexity of reactive code without the operational benefit.
If a path is blocking, isolate it and label it honestly. That is much easier to maintain.
Common Pitfalls
The most common mistake is wrapping a blocking result with Mono.just(...) after the blocking call already finished. That changes the type, not the behavior.
Another mistake is calling .block() inside WebFlux request handling. That reintroduces synchronous waiting and can cause the same thread starvation problems you were trying to avoid.
Teams also assume boundedElastic makes JDBC reactive. It does not. It only relocates the blocking work to a safer scheduler.
Finally, avoid scattering subscribeOn at random points in the chain. Keep the blocking boundary close to the blocking call so the code remains obvious.
Summary
- JDBC is blocking, so it must not run on WebFlux event-loop threads.
- Wrap JDBC calls in
Mono.fromCallableand useSchedulers.boundedElastic(). - Keep the blocking boundary in the service layer.
- Use R2DBC if the application truly needs end-to-end reactive database access.
- Reactive return types do not make blocking code non-blocking by themselves.

