Reactive vs Traditional Microservices in Spring Boot

java
Reactive vs Traditional Microservices in Spring Boot

Hello.

These are some notes I put together while comparing traditional (imperative) microservices with reactive microservices in Spring Boot. It started as a simple question — "what's the actual difference between Spring MVC and WebFlux?" — and ended up touching folder structure, hexagonal architecture, and a concrete high-concurrency use case (a login service), so I decided to organize it into a single article.

1. Traditional Microservice (Imperative)

  • Based on Spring MVC.
  • Uses a blocking, synchronous model.
  • Each request is handled on a dedicated thread from the server's thread pool.
  • If an I/O operation (a database call, or a call to another service) takes time, that thread stays blocked waiting for the response.
  • Scalability is limited because the number of threads is finite.

Example with Spring MVC (imperative)

@RestController
@RequestMapping("/customers")
public class CustomerController {

    @Autowired
    private CustomerService customerService;

    @GetMapping("/{id}")
    public ResponseEntity<Customer> getCustomer(@PathVariable Long id) {
        Customer customer = customerService.findCustomer(id);
        return ResponseEntity.ok(customer);
    }
}

Here, each HTTP request occupies a separate thread, and if the database takes time to respond, that thread stays blocked.

2. Reactive Microservice

  • Based on Spring WebFlux.
  • Uses a non-blocking, asynchronous model.
  • Uses a single, small thread pool to handle many requests with fewer resources.
  • Uses Reactor (Mono, Flux) for reactive programming.
  • A good fit for high-concurrency scenarios, or when the response time of external services varies a lot.

Example with Spring WebFlux (reactive)

@RestController
@RequestMapping("/customers")
public class CustomerController {

    @Autowired
    private CustomerService customerService;

    @GetMapping("/{id}")
    public Mono<ResponseEntity<Customer>> getCustomer(@PathVariable Long id) {
        return customerService.findCustomer(id)
                .map(customer -> ResponseEntity.ok(customer))
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }
}

Here, Mono<Customer> lets us handle the response in a non-blocking way.

3. When to Use Each One

Characteristic Traditional (Spring MVC) Reactive (Spring WebFlux)
Concurrency handling Blocking (1 thread per request) Non-blocking (fewer threads, more efficient)
Scalability Limited by the number of threads Better scalability with fewer resources
Use cases Simple CRUDs, low concurrency High concurrency, data streaming
Database support JDBC (blocking) R2DBC, MongoDB, Redis (non-blocking)
External API integration Blocking REST client Non-blocking REST client (WebClient)

Is it worth using Spring WebFlux?

Yes, if you're dealing with high concurrency, data streaming, or a lot of calls to external services. ❌ No, if your application is a regular CRUD backed by a traditional relational database.

If you come from a background heavy on Java and relational databases (banking-style backends, for example), chances are you've worked mostly with Spring MVC and JDBC. But if improving the scalability and performance of your microservices is something you care about, learning Spring WebFlux is worth the investment.

4. How the Folder Structure Changes

In a traditional microservice with Spring MVC, the folder layout follows the classic Model-View-Controller pattern. In a reactive microservice with Spring WebFlux, the layout can look similar, or it can shift to a functional style built around handlers and routers instead of traditional controllers.

4.1 Traditional Architecture (Spring MVC)

src/main/java/com/company/myservice/
├── controller/          # REST controllers (Spring MVC)
│   ├── CustomerController.java
├── service/             # Business logic
│   ├── CustomerService.java
├── repository/          # Data access (Spring Data JPA)
│   ├── CustomerRepository.java
├── model/               # Entity classes (JPA)
│   ├── Customer.java
├── dto/                 # DTO classes for JSON responses
│   ├── CustomerDto.java
├── exception/           # Custom error handling
├── config/              # Configuration (security, CORS, etc.)
└── MyServiceApplication.java   # Main class

Data flow in a traditional microservice:

  1. Controller receives an HTTP request.
  2. It calls a Service, which implements the business logic.
  3. Service uses a Repository to access the database through JPA.
  4. The response goes back to the client wrapped in a ResponseEntity<>.

4.2 Reactive Architecture (Spring WebFlux)

In Spring WebFlux, the architecture changes because execution is asynchronous and non-blocking. You can either keep traditional annotated controllers, or move to a structure based on handlers and routers.

Option 1: MVC-like, but reactive

src/main/java/com/company/myservice/
├── controller/          # REST controllers (Spring WebFlux)
│   ├── CustomerController.java
├── service/             # Reactive business logic
│   ├── CustomerService.java
├── repository/          # Data access (Spring Data R2DBC, MongoDB)
│   ├── CustomerRepository.java
├── model/               # Entity classes (non-blocking)
│   ├── Customer.java
├── dto/                 # DTO classes
│   ├── CustomerDto.java
├── exception/           # Error handling
├── config/              # Reactive configuration
└── MyServiceApplication.java

Option 2: Handlers and Routers (functional style)

src/main/java/com/company/myservice/
├── handler/             # Request handling via Handlers (instead of Controllers)
│   ├── CustomerHandler.java
├── router/              # Route configuration (instead of @RequestMapping)
│   ├── CustomerRouter.java
├── service/             # Reactive business logic
│   ├── CustomerService.java
├── repository/          # Data access with R2DBC or MongoDB
│   ├── CustomerRepository.java
├── model/               # Entity classes
│   ├── Customer.java
├── dto/                 # DTO classes
├── exception/           # Error handling
├── config/              # Configuration
└── MyServiceApplication.java

Data flow in a functional reactive architecture:

  1. Router forwards the request to the Handler.
  2. Handler runs the logic and calls the Service.
  3. Service returns a Mono<> or Flux<> with data coming from the Repository.
  4. The result is sent to the client without blocking any thread.

4.3 Key Architectural Differences

Characteristic Traditional MVC Reactive (WebFlux)
Controller @RestController with @RequestMapping @RestController or Handler + Router
Data access Blocking methods (Customer findById()) Non-blocking methods (Mono<Customer> findById())
Repository JpaRepository (blocking) R2dbcRepository or ReactiveCrudRepository
Threads 1 thread per request (blocking) Fewer threads, efficient execution
Scalability Less scalable under high load More scalable and efficient

Which one should you use?

Spring MVC (traditional) — CRUDs backed by relational databases; low-concurrency applications; you don't have significant I/O blocking to worry about.

Spring WebFlux (reactive) — microservices handling many simultaneous connections; systems with real-time data streaming; integrations with NoSQL (MongoDB, Redis) or asynchronous external APIs.

If your microservice is simple and JPA/SQL-based, sticking with the traditional MVC model is the right call. If you need high concurrency and efficiency, WebFlux is a powerful option.

5. When Does Hexagonal Architecture Make Sense?

This depends on the size and complexity of the microservice:

📌 If the microservice is small and doesn't have many external dependencies, a classic layered architecture (MVC-adapted) is enough. 📌 If the microservice is going to scale, talks to several external dependencies (databases, queues, APIs), or needs high maintainability, hexagonal architecture is worth the investment.

Scenario Traditional (MVC/Layers) Hexagonal Architecture
Small, simple microservice ✅ Easier to implement ❌ Unnecessary overhead
Talks to a single DB and few services ✅ MVC is enough ❌ Hexagonal can be overkill
Expected growth of the service ❌ Can become rigid ✅ Makes scaling easier
Many external dependencies (DBs, queues, external APIs) ❌ Hard to maintain ✅ More modular and maintainable
Need for isolated unit tests ❌ Coupling gets in the way ✅ Hexagonal separates dependencies

General recommendation:

  • For small microservices, start with MVC + good practices (clear layer separation, DTOs, services, repositories).
  • If the service starts growing and needs more flexibility, refactor towards hexagonal architecture.
  • If you already know from day one that it's going to be complex (event handling, multiple data sources, external APIs), it's worth starting hexagonal from the beginning.

It's not mandatory to always use hexagonal architecture, but it's a solid investment for systems that will scale or accumulate many integrations.

6. A Concrete Case: Reactive + Hexagonal for a High-Concurrency Login Service

This is the scenario that tied the whole comparison together for me: a login/authentication service expected to handle high concurrency. In that case, combining hexagonal architecture with reactive programming is a strong choice if you're after performance and scalability.

Why hexagonal + reactive for a high-concurrency login?

High throughput: WebFlux and R2DBC (instead of JDBC) handle many connections without blocking threads. ✅ Scalability: thousands of concurrent requests without overloading the server. ✅ Decoupling: you can swap the database or the authentication mechanism without touching the domain logic. ✅ Testability: adapters (repositories, controllers) can be tested in isolation.

Recommended structure (reactive login, hexagonal architecture):

src/main/java/com/company/auth/
├── application/          # Use cases
│   ├── service/
│       ├── AuthService.java        # Authentication logic (JWT, OAuth, etc.)
│       ├── UserService.java        # User management
├── domain/               # Model and business rules
│   ├── model/
│       ├── User.java              # User entity
│       ├── Role.java              # User roles
│   ├── repository/
│       ├── UserRepository.java    # Persistence interface
├── infrastructure/       # Adapters
│   ├── controller/
│       ├── AuthController.java    # REST endpoints (WebFlux)
│   ├── repository/
│       ├── UserRepositoryImpl.java  # Implementation with R2DBC or MongoDB
│   ├── security/
│       ├── JwtUtil.java           # JWT generation
│       ├── SecurityConfig.java    # Security configuration (Spring Security + WebFlux)
└── MyServiceApplication.java      # Main class

Recommended technologies:

  • Spring Boot + WebFlux — handles concurrent requests without blocking.
  • Spring Security with JWT/OAuth2 — secure, efficient authentication.
  • R2DBC (PostgreSQL, MySQL) or MongoDB — reactive, scalable databases.
  • Redis — session storage, to avoid overloading the database.

How does this help avoid bottlenecks?

  • With WebFlux, requests don't block threads, which reduces resource consumption.
  • With R2DBC or MongoDB, you avoid the database-level blocking problem.
  • With Redis, you can store tokens and avoid hitting the database constantly.

Conclusion

If you expect high concurrency, combining hexagonal architecture with reactive programming is a strong choice — you gain scalability and performance without giving up a clean design.

That said, none of this is a default. For a small, low-concurrency service backed by a relational database, plain Spring MVC with good layering is still the simplest and most maintainable option. The decision should follow the actual load and integration profile of the service, not a general preference for "the more modern stack."

Thanks for reading.

Comments