NashTech Blog

The tale of cities in Springville: MVC & WebFlux

Table of Contents

In the digital realm of Java frameworks, there existed a prosperous domain called Spring Boot. Within this domain, two magnificent cities had been built over time: the ancient city called MVC and the newer, city called WebFlux. Both cities served the same purpose—helping developers create powerful web applications—but they did so in fundamentally different ways.

MVC: A Traditional Stronghold

In the city of MVC (Model-View-Controller), lived a diligent developer named Maya. Maya had been building web applications for years using Spring Boot’s traditional approach. Every morning, she would wake up and handle HTTP requests one by one, in a synchronous fashion.

“Each request gets its own thread from the thread pool,” Maya would explain to newcomers. “It’s like having a dedicated worker for each customer who walks into our shop. They handle the customer’s request from start to finish before moving on to the next one.”

The MVC city operated on a simple principle: when a request arrived, a thread from the thread pool would be assigned to process it. This thread would handle all aspects of the request—retrieving data from the database, processing it, and returning a response—before becoming available again for another request.

This approach had served the domain well for many years. It was straightforward, easy to understand, and worked reliably for most applications. The code in MVC city looked familiar to most Java developers:

@RestController
@RequestMapping("/customers")
public class CustomerController {
    
    private final CustomerRepository customerRepository;
    
    public CustomerController(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }
    
    @GetMapping
    public List<Customer> getAllCustomers() {
        // This blocks the thread until all customers are retrieved
        return customerRepository.findAll();
    }
    
    @GetMapping("/{id}")
    public Customer getCustomerById(@PathVariable Long id) {
        // This blocks the thread until the customer is found
        return customerRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Customer not found"));
    }
    
    @PostMapping
    public Customer createCustomer(@RequestBody Customer customer) {
        // This blocks the thread until the customer is saved
        return customerRepository.save(customer);
    }
    
    // Other CRUD operations...
}

The Rise of WebFlux: A Reactive Revolution

As the digital space grew, certain challenges began to emerge. Some applications needed to handle thousands of concurrent connections, and others required integrating with slow external services. The thread-per-request model of MVC city started showing its limitations.

This is when a brilliant architect named Raj began exploring a different approach. Raj had studied the reactive programming paradigm and believed it could solve these emerging challenges. Thus, the city of WebFlux was founded.

“In WebFlux,” Raj would explain, “we don’t assign a dedicated thread to each request. Instead, we use a small, fixed number of threads that handle many requests concurrently. It’s like having a few skilled multitaskers rather than many single-focused workers.”

WebFlux city was built on the principle of non-blocking, reactive programming. When a request arrived, it would be processed in stages. If a stage involved waiting (for example, for a database query to complete), the thread wouldn’t sit idle. Instead, it would move on to process parts of other requests and return to the original request when the waiting was over.

@RestController
@RequestMapping("/customers")
public class ReactiveCustomerController {
    
    private final ReactiveCustomerRepository customerRepository;
    
    public ReactiveCustomerController(ReactiveCustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }
    
    @GetMapping
    public Flux<Customer> getAllCustomers() {
        // This returns immediately with a Flux (a stream of customers)
        // The thread is free to handle other requests while data is being retrieved
        return customerRepository.findAll();
    }
    
    @GetMapping("/{id}")
    public Mono<Customer> getCustomerById(@PathVariable Long id) {
        // This returns immediately with a Mono (a promise of a future customer)
        return customerRepository.findById(id)
                .switchIfEmpty(Mono.error(new ResourceNotFoundException("Customer not found")));
    }
    
    @PostMapping
    public Mono<Customer> createCustomer(@RequestBody Customer customer) {
        // This returns immediately with a Mono (a promise of the saved customer)
        return customerRepository.save(customer);
    }
    
    // Other CRUD operations...
}

Conflicting Needs

One day, a young developer named Tara came to the kingdom with a new application to build. She was confused about which city to choose for her residence—MVC or WebFlux.

Both Maya and Raj invited Tara to spend a day with them to understand the strengths and limitations of each approach.

Maya showed how MVC’s simplicity made it perfect for most traditional web applications where the number of concurrent connections was modest and the operations weren’t particularly I/O intensive.

“MVC is like a comfortable old shoe,” Maya explained. “It’s familiar, reliable, and gets the job done for most scenarios. If your application doesn’t need to handle thousands of concurrent connections or if you’re working with blocking libraries that don’t have reactive alternatives, MVC is still your best choice.”

The next day, Raj demonstrated the power of WebFlux for applications that needed to handle many concurrent connections with a small number of threads.

“WebFlux shines when you’re building applications that need to scale to handle many simultaneous users with limited resources,” Raj explained. “It’s also the right choice when you’re dealing with operations that involve a lot of waiting—like calling multiple microservices or processing streams of data.”

When to Choose WebFlux: Real-world Scenarios

Tara was intrigued by WebFlux but needed concrete scenarios to understand when to choose it over MVC. Raj offered several compelling use cases:

  • High-concurrency applications: “If your system needs to handle thousands of concurrent connections, WebFlux can do so with fewer resources than MVC.”
  • Microservices orchestration: “When your application needs to call multiple microservices and combine their results, WebFlux’s non-blocking approach prevents threads from sitting idle while waiting for responses.”
  • Real-time data processing: “For applications that deal with streams of data, like monitoring systems or chat applications, WebFlux’s reactive nature makes it a natural fit.”
  • Resource-constrained environments: “In environments where you need to maximize the throughput with limited CPU and memory resources, WebFlux’s efficiency can be a game-changer.”
  • Long-lived connections: “For applications using WebSockets or Server-Sent Events (SSE), WebFlux’s non-blocking model is more resource-efficient.”

The Complete WebFlux CRUD Implementation

To help Tara understand how a complete CRUD application would look in WebFlux, Raj shared a comprehensive example:

First, he showed her the model:

@Data
@Document
public class Customer {
    @Id
    private String id;
    private String firstName;
    private String lastName;
    private String email;
    private LocalDate birthDate;
}
```

Then, the reactive repository:

```java
public interface ReactiveCustomerRepository extends ReactiveCrudRepository<Customer, String> {
    // Find all customers with a specific last name
    Flux<Customer> findByLastName(String lastName);
    
    // Find customer by email
    Mono<Customer> findByEmail(String email);
}

@RestController
@RequestMapping("/api/customers")
public class CustomerController {
    
    private final ReactiveCustomerRepository repository;
    
    public CustomerController(ReactiveCustomerRepository repository) {
        this.repository = repository;
    }
    
    // Create a new customer
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<Customer> createCustomer(@RequestBody Customer customer) {
        return repository.save(customer);
    }
    
    // Get all customers
    @GetMapping
    public Flux<Customer> getAllCustomers() {
        return repository.findAll();
    }
    
    // Get a single customer by ID
    @GetMapping("/{id}")
    public Mono<Customer> getCustomerById(@PathVariable String id) {
        return repository.findById(id)
                .switchIfEmpty(Mono.error(new ResponseStatusException(
                        HttpStatus.NOT_FOUND, "Customer not found with id: " + id)));
    }
    
    // Update a customer
    @PutMapping("/{id}")
    public Mono<Customer> updateCustomer(@PathVariable String id, @RequestBody Customer customer) {
        return repository.findById(id)
                .flatMap(existingCustomer -> {
                    existingCustomer.setFirstName(customer.getFirstName());
                    existingCustomer.setLastName(customer.getLastName());
                    existingCustomer.setEmail(customer.getEmail());
                    existingCustomer.setBirthDate(customer.getBirthDate());
                    return repository.save(existingCustomer);
                })
                .switchIfEmpty(Mono.error(new ResponseStatusException(
                        HttpStatus.NOT_FOUND, "Customer not found with id: " + id)));
    }
    
    // Delete a customer
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public Mono<Void> deleteCustomer(@PathVariable String id) {
        return repository.findById(id)
                .flatMap(customer -> repository.delete(customer))
                .switchIfEmpty(Mono.error(new ResponseStatusException(
                        HttpStatus.NOT_FOUND, "Customer not found with id: " + id)));
    }
    
    // Find customers by last name
    @GetMapping("/search")
    public Flux<Customer> findByLastName(@RequestParam String lastName) {
        return repository.findByLastName(lastName);
    }
}

The Challenge of Reactive Programming

Raj was honest about the challenges that came with WebFlux:

“The reactive programming model requires a different way of thinking,” he admitted. “Instead of thinking sequentially, you need to think in terms of data flows and transformations. The learning curve can be steep, especially if you’re used to imperative programming.”

He showed Tara some examples of common reactive patterns:

// Composing multiple operations
public Mono<CustomerSummary> getCustomerSummary(String customerId) {
    Mono<Customer> customerMono = repository.findById(customerId);
    Mono<List<Order>> ordersMono = orderRepository.findByCustomerId(customerId).collectList();
    
    return Mono.zip(customerMono, ordersMono)
            .map(tuple -> {
                Customer customer = tuple.getT1();
                List<Order> orders = tuple.getT2();
                return new CustomerSummary(customer, orders);
            });
}

// Error handling
public Mono<Customer> getCustomerWithFallback(String id) {
    return repository.findById(id)
            .onErrorResume(e -> {
                log.error("Error fetching customer", e);
                return Mono.just(new Customer()); // Fallback customer
            })
            .switchIfEmpty(Mono.just(new Customer())); // Default if not found
}

The Decision

After spending time in both cities, Tara had to make a decision for her new application. She was building a real-time dashboard that needed to handle data from multiple sources and support thousands of concurrent users.

“I think WebFlux is the right choice for my application,” Tara concluded. “The non-blocking, reactive approach aligns perfectly with my requirements for high concurrency and real-time data processing.”

Maya nodded in agreement. “I think that’s the right decision for your specific needs. Remember, it’s not about which city is better in absolute terms—it’s about which one is better suited for your particular journey.”

Raj added, “And remember, the two cities aren’t at war with each other. They’re different neighborhoods in the same kingdom of Spring Boot, each with its own strengths and purpose.”

Picture of Vimal Kumar

Vimal Kumar

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top