You type a URL, hit enter, and JSON comes back. Between those two moments, your Spring Boot app runs a small relay race: the request passes through an embedded web server, a front controller, and a few helpers before your code ever runs. Once you can picture that path, debugging gets much easier - a 404, a missing parameter, and a broken interceptor all fail at different stops on the same road.

Let's follow one GET request end to end, with a real app you can run yourself.

The five stops

Here is the whole journey, animated. Watch the request packet travel from the client to the controller and the response come back:

Animated walkthrough of an HTTP request traveling through embedded Tomcat, the DispatcherServlet, an interceptor and the controller, then back as JSON

In plain words:

  1. The client sends the request. A browser, curl, or another service sends GET /api/greet?name=Vishnu.
  2. The embedded server accepts it. Spring Boot runs its own web server inside the app. Per the Spring Boot reference, "Spring Boot includes support for embedded Tomcat and Jetty servers", and by default it "listens for HTTP requests on port 8080". No separate server install - main() starts everything.
  3. The DispatcherServlet takes over. Spring MVC is "designed around the front controller pattern where a central Servlet, the DispatcherServlet, provides a shared algorithm for request processing, while actual work is performed by configurable delegate components" (Spring Framework docs). In practice: one servlet receives everything, then looks up which @GetMapping matches the path.
  4. Interceptors get a look. Before and after your controller, interceptors can inspect or reject the request - good for logging, auth checks, and timing.
  5. Your controller runs. The method returns data, Spring converts it to JSON, and the response travels back out the way it came.

A real app that shows its own journey

This is a complete Spring Boot app. The interceptor and controller print a line at each stop, so the server's own log narrates the relay:

package com.example.reqdemo; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.Map; @SpringBootApplication public class ReqDemoApplication { public static void main(String[] args) { SpringApplication.run(ReqDemoApplication.class, args); } @RestController static class GreetController { @GetMapping("/api/greet") public Map<String, String> greet(@RequestParam(defaultValue = "world") String name) { System.out.println("[controller] DispatcherServlet handed me GET /api/greet, name=" + name); return Map.of("message", "Hello, " + name + "!"); } } @Component static class FlowInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) { System.out.println("[interceptor] request passed the servlet and filters: " + req.getMethod() + " " + req.getRequestURI()); return true; } @Override public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception ex) { System.out.println("[interceptor] response written, status=" + res.getStatus() + " - request journey complete"); } } @Component static class WebConfig implements WebMvcConfigurer { private final FlowInterceptor interceptor; WebConfig(FlowInterceptor interceptor) { this.interceptor = interceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(interceptor); } } } 

I compiled and ran this on JDK 21 (Temurin 21.0.13) with Spring Boot 3.5.7 before publishing. Here is the real, unedited output.

Startup - the embedded Tomcat coming up inside the app (port 8090 here, because 8080 was already in use in the run environment):

Tomcat initialized with port 8090 (http) Tomcat started on port 8090 (http) with context path '/' Started ReqDemoApplication in 1.054 seconds 

Then curl http://localhost:8090/api/greet?name=Vishnu returned:

{"message":"Hello, Vishnu!"} 

And the app's own log narrated the journey, exactly in the order of the animation:

[interceptor] request passed the servlet and filters: GET /api/greet [controller] DispatcherServlet handed me GET /api/greet, name=Vishnu [interceptor] response written, status=200 - request journey complete 

Three things worth noticing:

  • The interceptor's preHandle runs before the controller, and afterCompletion runs after the response is written. Your controller is the middle of the sandwich, not the start.
  • Returning a Map was enough to get JSON. Spring Boot auto-configures an HTTP message converter that serializes the return value - you never wrote JSON yourself.
  • The journey back through the server is mostly invisible to your code, which is why the interceptor's completion callback is a handy place to log it.

A 404 travels the same road

One detail that surprises people: an unknown path goes through the same pipeline. Calling curl http://localhost:8090/api/nope produced this log:

[interceptor] request passed the servlet and filters: GET /api/nope [interceptor] response written, status=404 - request journey complete [interceptor] request passed the servlet and filters: GET /error [interceptor] response written, status=404 - request journey complete 

Two journeys for one request. The DispatcherServlet found no mapping for /api/nope, so it raised an error, and the servlet container then dispatched the request again, this time to Spring Boot's built-in /error endpoint - which is what generated the JSON error body:

{"timestamp":"2026-11-05T10:56:56.906+00:00","status":404,"error":"Not Found","path":"/api/nope"} 

So when you debug a 404, the first question is: did the request reach the DispatcherServlet at all? If your interceptor logged it, the server side is fine and the path or mapping is wrong. If it did not, the problem is earlier - wrong port, wrong context path, or something in front of the app.

When this mental model pays off

  • Debugging 404s and 405s: find out which stop rejected the request instead of guessing.
  • Placing cross-cutting logic: auth checks, request IDs, and timing belong in interceptors or filters, not copied into every controller.
  • Reading startup logs: Tomcat started on port ... tells you the embedded server is the entry point, and it is part of your app's process.

If dependency injection is how your beans find each other, the DispatcherServlet is how the outside world finds your beans. Both are quiet machinery that runs before your code does.

Sources

Written with AI assistance; code examples were compiled and run on JDK 21 before publishing. The animation is an illustration of the request flow, not a screen recording.