Hook: A REST request is like a visitor entering a building: security checks happen first, then the front desk routes them to the right person, and only then do you get the final answer back. Interviewers love this question because it shows whether you understand the full path, not just the controller.
Question: Explain REST API request flow.
Answer: In Spring Boot, a REST request starts at the HTTP server, passes through filters, reaches Spring's DispatcherServlet, gets matched to a controller method, and then the response is converted back to JSON. Along the way, interceptors, validation, exception handlers, and message converters can all participate. If something fails, Spring can stop the flow early and return a status like 400, 404, or 500.
Interview-Ready Answer: I would say: a REST API request in Spring Boot begins when the client sends an HTTP method, path, headers, and maybe a body. The embedded server receives it, filters and interceptors can inspect it, Spring routes it through DispatcherServlet to the correct controller, the controller usually calls a service or repository, and then Spring serializes the return value into JSON using Jackson. If something goes wrong, exception handling turns it into a consistent error response instead of a crash.
REST is an architectural style for exposing resources over HTTP. A resource is just a thing your API manages, such as a product, order, user, or payment. The request flow matters because in Spring Boot the request does not jump straight into your controller; it moves through several layers that can log, secure, validate, transform, or reject it.
GET, POST, or PUT, plus a path such as /api/products/42. It may also send headers like Content-Type and Accept, and for write requests it may include a JSON body.200 worker threads, so one slow database call can tie up a thread and reduce throughput.DispatcherServlet asks the configured HandlerMapping components which controller method matches the path and HTTP verb.HandlerInterceptor is higher-level than a filter because it already knows which handler was chosen. Its preHandle method runs before the controller, postHandle runs after the controller returns, and afterCompletion runs after the request is fully done.HttpMessageConverter implementations, most often Jackson for JSON. If the body cannot be parsed, the request may fail with 400 Bad Request before your business code does anything.Accept header and supported converters.@RestControllerAdvice or @ExceptionHandler can turn that exception into a clear JSON error body instead of the default HTML error page.| Component | When it runs | Knows the controller? | Best for |
|---|---|---|---|
| Filter | Before Spring MVC | No | Security, logging, CORS |
| Interceptor | After mapping, before controller | Yes | Metrics, auth checks, tracing |
| Controller advice | On exceptions | N/A | Consistent error bodies |
GET reads, POST creates, PUT replaces, PATCH updates partially, and DELETE removes.The routing part of a request is effectively O(1) per request in normal applications, because Spring uses cached mappings and direct method dispatch rather than scanning your code line by line. The expensive parts are usually JSON parsing, validation, network latency, and database I/O. Space usage is also close to O(1) per request, excluding payload size and buffers. In real systems, the thread pool is often the first bottleneck: if the server has about 200 worker threads and each request blocks on a slow database query for 500 ms, throughput drops quickly even though the REST code itself is simple.
405 Method Not Allowed.415 Unsupported Media Type.400 Bad Request.Memory from the whole flow: think door, desk, dispatcher, worker, wrapper. First the server opens the door, then filters at the desk, then the dispatcher routes, then the controller works, then the wrapper sends JSON back.
Real-World Story: In an e-commerce checkout service, a team added a custom security filter to inspect every incoming request. The filter accidentally read the JSON body while trying to log it. Because the request body is a stream, the controller later received an empty body and Jackson could not deserialize the order payload. Users saw failed checkouts, the API returned 400 Bad Request, and logs showed messages like Required request body is missing even though the client had clearly sent data.
The misunderstanding was simple but costly: the team thought the controller was the first place request data existed. In reality, the request had already passed through filters, and the body was consumed before Spring MVC could bind it. The fix was to stop reading the body in the filter or wrap the request in a caching wrapper when logging was necessary.
What it looked like in production: support tickets spiked, payment attempts failed only on POST endpoints, metrics showed a sudden rise in 400 responses, and the controller never printed its normal business logs. That is the kind of outage where knowing the request flow saves hours of guessing.
package com.example.demo;
import java.io.IOException;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@Configuration
class WebConfig implements WebMvcConfigurer {
@Bean
RequestLoggingFilter requestLoggingFilter() {
return new RequestLoggingFilter();
}
@Bean
RequestTimingInterceptor requestTimingInterceptor() {
return new RequestTimingInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestTimingInterceptor());
}
}
class RequestLoggingFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
log.info("[Filter] before chain: {} {}", request.getMethod(), request.getRequestURI());
try {
// A filter sees the request before Spring picks a controller, so it is ideal for cross-cutting concerns.
filterChain.doFilter(request, response);
} finally {
log.info("[Filter] after chain: {} {}", request.getMethod(), request.getRequestURI());
}
}
}
class RequestTimingInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(RequestTimingInterceptor.class);
private static final String START_NANOS = "startNanos";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
request.setAttribute(START_NANOS, System.nanoTime());
log.info("[Interceptor] preHandle before controller method");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
org.springframework.web.servlet.ModelAndView modelAndView) {
log.info("[Interceptor] postHandle after controller returns");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
Long start = (Long) request.getAttribute(START_NANOS);
if (start != null) {
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
log.info("[Interceptor] afterCompletion took {} ms", elapsedMs);
}
}
}
@Service
class ProductService {
Map<String, Object> findProduct(long id) {
// A service holds business logic so the controller stays thin and easy to test.
return Map.of(
"id", id,
"name", "Coffee Mug",
"price", 12.99,
"currency", "USD"
);
}
}
@RestController
@RequestMapping("/api/products")
class ProductController {
private final ProductService productService;
ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/{id}")
public Map<String, Object> getProduct(@PathVariable long id) {
// A bad path variable is rejected early; Spring turns it into a clean HTTP 400 instead of a null pointer.
if (id <= 0) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "id must be positive");
}
return productService.findProduct(id);
}
}
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Map<String, Object>> handleResponseStatus(ResponseStatusException ex,
HttpServletRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", Instant.now().toString());
body.put("status", ex.getStatusCode().value());
body.put("error", ex.getReason() != null ? ex.getReason() : "Request failed");
body.put("path", request.getRequestURI());
return ResponseEntity.status(ex.getStatusCode()).body(body);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleUnexpected(Exception ex, HttpServletRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", Instant.now().toString());
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
body.put("error", "Internal Server Error");
body.put("message", ex.getMessage());
body.put("path", request.getRequestURI());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
}
}
Follow-up & Tricky Questions:
DispatcherServlet? It is Spring MVC's front controller. It receives the request first, asks mappings which handler matches, and coordinates the rest of the flow.404 before my controller runs? Because no handler mapping matched that path and verb. Spring can reject the request during routing, so your method is never called.preHandle runs before the controller, and the request can be stopped there. Many candidates reverse this order.Common Mistakes:
Memory Hook: Door, desk, dispatcher, worker, wrapper. The request enters the door, passes the desk checks, gets dispatched, the worker does the job, and the wrapper sends the response back.
Cheat Sheet:
DispatcherServlet routes next.Practice Tasks:
POST and inspect how the body is deserialized.404, 400, and 500 intentionally, then observe which part of the flow handled each one.