RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Spring Boot questions
MediumSpring Boot#309 min readJul 11, 2026

Explain REST API request flow.

practice
learning
Practice modeTest yourself instead of reading straight through

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.

🧠 Memory Map
Memory map — visual summary of this topic

Big picture

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.

Request flow under the hood

  1. The client creates an HTTP request. It sends a method like 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.
  2. The server accepts the connection. In Spring Boot, this is usually the embedded servlet container, commonly Tomcat. A worker thread handles the request. Tomcat's default thread pool is commonly around 200 worker threads, so one slow database call can tie up a thread and reduce throughput.
  3. Servlet filters run first. A filter is a low-level gate that sees every request before Spring chooses a controller. This is where you usually put logging, authentication, CORS, compression, and request wrapping. If a filter decides the request is invalid, it can stop the flow immediately and return a response.
  4. DispatcherServlet acts as the front controller. A front controller is a single entry point that receives the request and routes it. Spring's DispatcherServlet asks the configured HandlerMapping components which controller method matches the path and HTTP verb.
  5. Interceptors can run before the controller. A 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.
  6. Spring binds request data to Java objects. Path variables, query parameters, and request bodies are converted into Java types. For JSON bodies, Spring uses 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.
  7. The controller runs. The controller should stay thin: it receives input, calls the service layer, and returns a result. The service layer contains business rules, and the repository layer talks to the database. This separation keeps request flow understandable and testable.
  8. Spring turns the return value into a response. If your controller returns a Java object, Spring serializes it into JSON, sets the status code, and writes headers. This is content negotiation: the server chooses a response format based on the request's Accept header and supported converters.
  9. Exception handling creates consistent errors. If an exception is thrown, Spring consults its exception resolvers. A @RestControllerAdvice or @ExceptionHandler can turn that exception into a clear JSON error body instead of the default HTML error page.
  10. The response travels back through the chain. Interceptors get their cleanup callback, filters finish their post-processing, and the server sends the final HTTP response to the client.

Where different pieces fit

ComponentWhen it runsKnows the controller?Best for
FilterBefore Spring MVCNoSecurity, logging, CORS
InterceptorAfter mapping, before controllerYesMetrics, auth checks, tracing
Controller adviceOn exceptionsN/AConsistent error bodies

When and why to use REST

  • Use REST when you want a stateless request-response model that is easy for web and mobile clients to consume.
  • Use HTTP verbs to describe intent: GET reads, POST creates, PUT replaces, PATCH updates partially, and DELETE removes.
  • REST is a good fit for microservices because each call is independent and easy to route through load balancers and caches.

Performance and limits

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.

Important edge cases

  • Body consumed too early: if a filter reads the request body directly, the controller may see an empty stream later. Use a caching wrapper when you truly need the body twice.
  • Wrong HTTP method: the path may exist, but using the wrong verb can produce 405 Method Not Allowed.
  • Wrong content type: sending XML to a JSON endpoint can produce 415 Unsupported Media Type.
  • Binding and validation errors: bad numbers, missing fields, or invalid bean validation constraints usually become 400 Bad Request.
  • Async work: if a controller returns a deferred result or uses async processing, the request may continue on a different thread, so thread-local assumptions can break.

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.

Spring Boot
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:

  • What is the role of 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.
  • What is the difference between a filter and an interceptor? A filter lives below Spring MVC and does not know which controller will run. An interceptor lives inside Spring MVC and can see the selected handler, which makes it better for controller-aware logic.
  • How does Spring convert JSON into Java objects? It uses message converters, usually Jackson for JSON. The converter reads the body and binds it to the method parameter before the controller logic runs.
  • Where should validation happen? Basic binding errors and bean validation happen near the boundary of the controller. Business validation should stay in the service layer, where the real rules live.
  • Why might a request return 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.
  • Tricky: Does the controller always run before an interceptor? No. preHandle runs before the controller, and the request can be stopped there. Many candidates reverse this order.
  • Tricky: Can a filter safely read the request body? Not by default. The body is a stream, so reading it once can consume it; if you need the body later, use a caching wrapper.
  • Tricky: Is REST the same as JSON? No. REST is an architectural style; JSON is only a data format commonly used in REST APIs.

Common Mistakes:

  • Mixing up filters and interceptors. Correction: filters run before Spring MVC mapping; interceptors run after mapping and can see the chosen handler.
  • Forgetting that the body is a stream. Correction: if you read it early in a filter, you may starve the controller unless you wrap it for caching.
  • Thinking request flow only means controller execution. Correction: the real flow includes routing, binding, serialization, and exception handling too.
  • Returning raw exceptions to clients. Correction: use controller advice to produce stable JSON errors and meaningful HTTP status codes.

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:

  • Client sends method, path, headers, and sometimes body.
  • Embedded server receives the request on a worker thread.
  • Filters run first; DispatcherServlet routes next.
  • Interceptors can run before and after controller execution.
  • Spring binds data, calls your controller, then serializes the result.
  • Exceptions become HTTP error responses through advice or resolvers.

Practice Tasks:

  • Add a second endpoint with POST and inspect how the body is deserialized.
  • Trigger a 404, 400, and 500 intentionally, then observe which part of the flow handled each one.
  • Write a simple filter that logs headers, then confirm it runs before the controller log line.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

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); } }