How it works
The explanation, with examples you can run.
The journey of one request
@RestController@RequestMapping("/orders")class OrderController { private final OrderService orders; OrderController(OrderService orders) { this.orders = orders; } @PostMapping("/{customerId}") ResponseEntity<OrderView> create(@PathVariable long customerId, @RequestBody @Valid NewOrder body) { OrderView created = orders.create(customerId, body); return ResponseEntity.status(201).body(created); }}For POST /orders/7 with a JSON body, roughly this happens:
- Tomcat takes a thread from its pool (
server.tomcat.threads.max, 200 by default) and builds anHttpServletRequest. - Filters run in order: character encoding, Spring Security, any
Filterbeans of your own. - The
DispatcherServletasks eachHandlerMappingfor a handler.RequestMappingHandlerMappingmatches the path pattern and the HTTP method tocreate. - Interceptors registered for that path get
preHandle. - A
HandlerAdapterinvokes the method. First,HandlerMethodArgumentResolvers build each argument:@PathVariablefrom the URI template,@RequestParamfrom the query string,@RequestBodyby asking theHttpMessageConverterthat supports the request'sContent-Type(Jackson, forapplication/json) to read the body.@Validthen runs Bean Validation. - Your method runs.
- The return value is written by a converter that matches the client's
Acceptheader. For@ResponseBodyandResponseEntitythis happens inside the adapter, beforepostHandle. - If anything threw,
HandlerExceptionResolvers (including your@ControllerAdvicewith@ExceptionHandlermethods) turn the exception into a response.
Status codes that tell you which step failed
| You see | The request stopped at |
|---|---|
| 404 | No handler mapping matched the path |
| 405 | The path matched, the HTTP method did not |
| 415 | No converter can read the request's Content-Type |
| 406 | No converter can write what Accept asks for |
| 400 | An argument could not be bound or failed @Valid |
Threads are the budget
One request holds one Tomcat thread from start to finish, including every database call and HTTP call it makes. When the pool is exhausted, new connections queue in the accept backlog (server.tomcat.accept-count, 100 by default) and are then refused. Slow downstream calls without timeouts are the usual cause. On Java 21, spring.threads.virtual.enabled=true (Boot 3.2+) runs requests on virtual threads instead, so blocking no longer pins a scarce platform thread; but the database still has only as many connections as the pool allows.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.