skip to content

Can you mix reactive and imperative in one system? What are the legitimate ways vs the traps?

level: middleimportance: should knowfreq 45%

answer

  1. across services: mix freely
  2. one app = one web runtime (both → MVC)
  3. MVC can use WebClient
  4. WebFlux → blocking via boundedElastic
  5. trap = blocking on event loop

basics

~20 s

Yes at the system level: some services on WebFlux, others on MVC. Within one app you can't run both web stacks, but MVC can use WebClient, and WebFlux can bridge blocking calls via Schedulers.boundedElastic. The trap is a blocking call on the event loop.

solid answer

~40 s

Mixing is fine and common at the architecture level — a reactive gateway or streaming service alongside imperative CRUD services. Within a single Spring Boot app you pick one web runtime: if both spring-boot-starter-web and -webflux are present, Boot defaults to MVC. But the clients cross over: an MVC app can legitimately use the reactive WebClient (RestTemplate is in maintenance mode) as its HTTP client, blocking on the result if needed. On WebFlux you bridge unavoidable blocking work with Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic()), keeping the event loop clean. Since Spring 6, controllers can also return CompletableFuture or use Kotlin coroutines/suspend functions as a middle ground. The trap is any blocking call — JDBC, .block(), Thread.sleep — running directly on a Netty event-loop thread, which stalls all multiplexed requests. Keep the boundary explicit and offload deliberately.

code

java · 21 lines
java
// Legit: MVC (Tomcat) using WebClient and blocking on a worker thread
@RestController
class AggregatorMvc {
    private final WebClient client;
    AggregatorMvc(WebClient.Builder b) { this.client = b.build(); }

    @GetMapping("/summary/{id}")
    Summary summary(@PathVariable String id) {
        // OK on a Tomcat worker thread (NOT an event loop):
        return client.get().uri("/data/{id}", id)
                     .retrieve().bodyToMono(Summary.class)
                     .block();
    }
}

// Legit: WebFlux bridging an unavoidable blocking SDK off the event loop
@GetMapping("/legacy/{id}")
Mono<Report> legacy(@PathVariable String id) {
    return Mono.fromCallable(() -> blockingSdk.buildReport(id))
               .subscribeOn(Schedulers.boundedElastic());
}

go deeper

for a junior

Know you can mix stacks across services, and that one app runs one web runtime. WebClient can be used from MVC.

for a middle

Explain the boundedElastic bridge, WebClient replacing RestTemplate, MVC async return types, and the blocking-on-loop trap.

for a senior

Discuss drawing the reactive boundary at service seams, coroutines as an imperative-over-reactive bridge, and why half-reactive chains give cost without benefit.

for a principal

Design the system topology: reactive edge/streaming vs imperative core, contract boundaries, and governance so blocking never leaks onto event loops across teams.

## Two meanings of 'mixing' ### A) Across services (architecture level) — freely mixable Different services in a system can use different stacks. A typical shape: a **WebFlux** edge/gateway (Spring Cloud Gateway is reactive) fanning out to a set of **MVC** domain services on blocking JDBC. They talk over HTTP/messaging, so each service's internal model is its own concern. This is the most common and lowest-risk form of mixing — use reactive precisely where its strengths matter (fan-out, streaming, connection scaling) and imperative everywhere else. ### B) Within one application — one web runtime only A single Boot app runs **one** web stack. `spring-boot-starter-web` pulls in Tomcat + `DispatcherServlet` (servlet/MVC); `spring-boot-starter-webflux` pulls in Netty + `DispatcherHandler` (reactive). If **both** are on the classpath, Boot chooses **MVC** by default (you can force `WebApplicationType.REACTIVE`). You don't serve some endpoints on Tomcat and others on Netty in the same process. So 'mixing within an app' really means mixing *programming styles around a single stack*, which is where nuance lives. ## Legitimate cross-over patterns 1. **MVC using WebClient.** `WebClient` (reactive, non-blocking HTTP client) is now the recommended client even in blocking apps — `RestTemplate` is in maintenance mode. In MVC you can call `webClient...retrieve().bodyToMono(T).block()` to get a value synchronously, or use it to run parallel downstream calls more efficiently than a thread-per-call `RestTemplate` loop. Blocking on the result is acceptable *because you're on a Tomcat worker thread, not an event loop*. 2. **WebFlux bridging to blocking code.** When a non-blocking driver doesn't exist, wrap the blocking call and move it off the event loop: ```java Mono.fromCallable(() -> blockingSdk.fetch(id)) .subscribeOn(Schedulers.boundedElastic()); ``` `boundedElastic` is a purpose-built, growable-but-capped pool for blocking bridges. Do this *deliberately and sparingly* — if it's most of your app, choose MVC. 3. **Async return types on MVC.** Spring MVC handlers can return `CompletableFuture<T>`, `DeferredResult<T>`, `Callable<T>`, or even a `Flux`/`Mono` (adapted) to free the servlet thread during async work — a middle ground without adopting the full reactive stack. 4. **Kotlin coroutines.** On WebFlux, `suspend` functions and `Flow` give imperative-looking code over the reactive runtime, easing the learning-curve cost while staying non-blocking. ## The traps - **Blocking on the event loop.** The cardinal sin: JDBC/JPA, `RestTemplate`, `Thread.sleep`, synchronous file/crypto calls, or `.block()` on a `reactor-http-nio-*` thread. `.block()` actually throws on those threads; silent blockers (JDBC) don't throw but stall the loop. Use `BlockHound` to catch them. - **Assuming both stacks coexist per-endpoint in one app.** They don't; it's one runtime. - **Half-reactive chains.** A reactive controller feeding a blocking service gives you reactive's complexity with none of its scaling benefit (the blocking layer caps you). Either commit the chain to non-blocking or keep it MVC. - **Forgetting `RestTemplate` isn't 'reactive' just because you called it inside a `map`.** Wrapping a blocking client in an operator doesn't make it non-blocking; it still blocks whatever thread runs it. ## Practical guidance Draw the reactive boundary at the *service* level where possible. Inside an app, keep the stack uniform and use the documented bridges (`boundedElastic`, `WebClient`, async return types, coroutines) at explicit seams — never blocking silently on the loop.

  • If both spring-boot-starter-web and spring-boot-starter-webflux are on the classpath, which stack starts?
    Spring Boot defaults to MVC (servlet, Tomcat) when both are present. To run WebFlux you must explicitly set the web application type to REACTIVE (e.g., via SpringApplication setWebApplicationType or spring.main.web-application-type=reactive). You don't get both stacks serving in one process.
  • Is it fine to call WebClient...block() in an MVC controller?
    Yes — you're on a Tomcat worker thread designed to block, so blocking there is normal thread-per-request behavior. The same .block() on a WebFlux Netty event-loop thread would throw and, if it didn't, would stall the loop. Context (which thread) is what makes it OK or not.

saying these in an interview costs you the question

  • Claiming a single app can serve some endpoints on Netty and others on Tomcat simultaneously
  • Saying wrapping RestTemplate/JDBC inside map() makes it non-blocking
  • Calling .block() inside a WebFlux handler
  • Building a reactive controller over a fully blocking service and expecting scaling gains

context