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>
server.tomcat.threads.max=200
server.tomcat.threads.min-spare=20
management.endpoints.web.exposure.include=health,metrics,threaddump
-
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);
}
}
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"
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);
// Fix: Offload to @Async service
@Async
public CompletableFuture<String> fetchExternalData() {
return CompletableFuture.completedFuture(restTemplate.getForObject(...));
}
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);
// Fix: Spring Retry with exponential backoff
@Retryable(retryFor = DataAccessException.class, maxAttempts = 3)
public void saveOrder(Order order) {
orderRepository.save(order);
}
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
# Fix: Match pool to thread capacity
spring.datasource.hikari.maximum-pool-size=200
Too few database connections cause thread starvation. Align Hikari pool size to Tomcat’s threads.max.
Summary
- Configure
server.tomcat.threads.maxto match expected concurrent users and monitor via/actuator/metrics - Use
@Asyncwith a customThreadPoolTaskExecutorfor I/O-bound operations to prevent thread blocking - Set
spring.datasource.hikari.maximum-pool-sizeequal to Tomcat threads to avoid database connection starvation
The author publishes Spring Boot starter templates at https://gumroad.com
Top comments (0)