DEV Community

Nitish
Nitish

Posted on

Spring Boot 3 and JVM Resilience in API Development: Avoiding JavaScript’s 2014 Pitfalls

Spring Boot 3 and JVM Resilience in API Development: Avoiding JavaScript’s 2014 Pitfalls

Without proper thread pooling and structured error handling, your production APIs might face cascading failures under load—like Node.js applications did during JavaScript’s scalability debates in 2014—while Spring Boot’s thread-per-request model remains unmonitored and unoptimized.

Prerequisites

  • Java 17 or later
  • Spring Boot 3.2.4
  • Spring Web and Spring Data JPA dependencies
  • Docker 24.0+ and PostgreSQL 15 image
  • Postman or curl for API testing

Configuring Spring Boot for Concurrent Request Handling

Spring Boot’s default Tomcat configuration handles 100 simultaneous requests, but tuning is critical for high-throughput APIs. Add these dependencies to optimize thread pools and metrics:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode
server.tomcat.threads.max=200
server.tomcat.threads.min-spare=20
management.endpoints.web.exposure.include=health,metrics,threaddump
Enter fullscreen mode Exit fullscreen mode
  • server.tomcat.threads.max: Maximum worker threads for concurrent requests
  • management.endpoints.web.exposure.include: Enables actuator metrics for monitoring thread usage

Implementing Resilient Order Processing with @RestController

This REST controller uses Spring’s thread-per-request model to avoid JavaScript-style event-loop blocking, with explicit error handling for database failures:

package com.example.orderservice;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.dao.DataAccessException;

@RestController
@RequestMapping("/orders")
public class OrderController {

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public String createOrder(@RequestBody OrderRequest request) {
        try {
            // Simulate database operation
            processOrder(request);
            return "Order processed";
        } catch (DataAccessException e) {
            throw new ServiceUnavailableException("Database write failed");
        }
    }

    private void processOrder(OrderRequest request) {
        // Business logic here
    }
}

@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
class ServiceUnavailableException extends RuntimeException {
    ServiceUnavailableException(String message) {
        super(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Test with curl -X POST http://localhost:8080/orders -H "Content-Type: application/json" -d '{"productId": 123}'. Verify thread usage via /actuator/metrics/tomcat.threads.busy.

Dockerizing PostgreSQL for Spring Data JPA

# docker-compose.yml
version: '3.8'
services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: orders
      POSTGRES_USER: spring
      POSTGRES_PASSWORD: secure
    ports:
      - "5432:5432"
Enter fullscreen mode Exit fullscreen mode

Run docker-compose up before starting the Spring Boot app to ensure database connectivity.

Common Mistakes

Mistake 1: Blocking the main thread with synchronous I/O

// Wrong: Synchronous HTTP call in controller
String result = restTemplate.getForObject("https://slow-api", String.class);
Enter fullscreen mode Exit fullscreen mode
// Fix: Offload to @Async service
@Async
public CompletableFuture<String> fetchExternalData() {
    return CompletableFuture.completedFuture(restTemplate.getForObject(...));
}
Enter fullscreen mode Exit fullscreen mode

Synchronous calls in controllers exhaust worker threads. Use @Async with a configured task executor.

Mistake 2: Missing retries for transient database errors

// Wrong: No retry for JPA operations
orderRepository.save(order);
Enter fullscreen mode Exit fullscreen mode
// Fix: Spring Retry with exponential backoff
@Retryable(retryFor = DataAccessException.class, maxAttempts = 3)
public void saveOrder(Order order) {
    orderRepository.save(order);
}
Enter fullscreen mode Exit fullscreen mode

Add @EnableRetry and spring-retry dependency to recover from transient database issues.

Mistake 3: Overlooking connection pool limits

# Wrong: Default Hikari pool size
spring.datasource.hikari.maximum-pool-size=10
Enter fullscreen mode Exit fullscreen mode
# Fix: Match pool to thread capacity
spring.datasource.hikari.maximum-pool-size=200
Enter fullscreen mode Exit fullscreen mode

Too few database connections cause thread starvation. Align Hikari pool size to Tomcat’s threads.max.

Summary

  • Configure server.tomcat.threads.max to match expected concurrent users and monitor via /actuator/metrics
  • Use @Async with a custom ThreadPoolTaskExecutor for I/O-bound operations to prevent thread blocking
  • Set spring.datasource.hikari.maximum-pool-size equal to Tomcat threads to avoid database connection starvation

The author publishes Spring Boot starter templates at https://gumroad.com

Top comments (0)