Servlet model & DispatcherServlet flow

java dev.to

Every Spring web request goes through one door

When you build a web app with Spring, you write small methods that handle URLs — one for /users, another for /orders, and so on. It feels like each method is wired straight to the network. It isn't.

Behind the scenes, every incoming request for your app is first handed to a single Java object. That object reads the request, works out which of your methods should answer it, calls that method, and turns whatever it returns into a response. That one object is the DispatcherServlet, and it sits at the center of everything Spring MVC does.

To see why it exists — and what it actually does on each request — we have to start one level below Spring, with the plain Java technology it is built on.

The Servlet: Java's original web building block

Long before Spring, Java already had a standard way to answer web requests: the Servlet. A servlet is just a Java class that receives an HTTP request and writes an HTTP response. That is the whole idea.

Here is the shape of one:

public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.setContentType("text/plain");
        resp.getWriter().write("Hello, world");
    }
}
Enter fullscreen mode Exit fullscreen mode

doGet runs when a GET request arrives. The req object hands you everything about the incoming request — the path, the headers, the query parameters. The resp object is where you write what goes back. That is genuinely all a servlet is.

But a servlet cannot run on its own. Something has to listen on the network port, accept the raw bytes of a connection, parse them into that tidy HttpServletRequest, and then decide which servlet to hand it to. That something is the servlet container — a program (Tomcat is the usual one) that owns the HTTP socket, manages a pool of threads, and controls the lifecycle of your servlets. You write servlets; the container runs them and feeds them requests.

How the container knows which servlet to call

A container can hold many servlets, so it needs a rule for matching a URL to one of them. That rule is a servlet mapping — a pattern that says "requests to this path go to that servlet."

@WebServlet("/hello")
public class HelloServlet extends HttpServlet { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

Here the mapping is /hello. A request to /hello lands in this servlet's doGet; a request to /goodbye does not. The container keeps a little table of these patterns and consults it for every request.

This works, and for one or two endpoints it is fine. The trouble starts when the app grows.

Why raw servlets stop scaling

Picture a real application: users, orders, invoices, search, each with several operations. With plain servlets, every distinct piece of behavior tends to become its own servlet with its own mapping. You accumulate dozens of them.

Worse, each one repeats the same chores. Read the request body and parse the JSON. Validate the fields. Catch exceptions and turn them into a sensible error response. Set the right content type. None of that is your actual business logic, yet it gets copied into servlet after servlet.

And the routing is rigid. The mapping table lives in the container's configuration, wired up before your logic ever runs. There is no single place where a request first lands — no natural spot to add a behavior that should apply to every request, like logging or a security check.

What you really want is one servlet that catches everything, does the shared chores once, and then routes each request to the right small piece of your code. That idea has a name.

The front controller idea

A front controller is a single entry point that receives every request for an application, performs the common work in one place, and then delegates to whichever component actually handles that specific request.

Instead of the container knowing about your /users and /orders logic directly, it knows about exactly one servlet. That servlet becomes the front door. Everything comes through it, and it decides where each request goes next.

This is not a Spring invention — it is a classic pattern. But Spring MVC's entire design rests on it, and Spring ships a ready-made front controller so you never write one yourself.

DispatcherServlet: Spring's front controller

The DispatcherServlet is that front controller, implemented as an ordinary servlet. When you use Spring MVC, Spring registers this one servlet with the container and maps it to a broad pattern — typically /, meaning "send me everything."

You do not write it or wire it up by hand. In a Spring Boot app it is registered automatically at startup. The mapping looks, conceptually, like this:

// You never write this — Spring Boot does it for you at startup.
// One servlet, mapped to "/", catches every request into the app.
registration.addServlet("dispatcherServlet", new DispatcherServlet(context));
registration.addMapping("/");
Enter fullscreen mode Exit fullscreen mode

The important shift: the servlet container now has essentially one servlet to think about. Every request funnels into the DispatcherServlet, and from there Spring — not the container — decides what happens.

But if there is only one servlet, how does a request to /users reach a different method than a request to /orders? That routing has moved inside Spring, and it needs a few helpers to do the job.

The pieces the DispatcherServlet delegates to

The DispatcherServlet does not contain your endpoint logic, and it does not hard-code the routing rules either. It orchestrates. To do that, it leans on a small set of collaborators, each with one responsibility:

  • A handler is the specific piece of your code that answers one kind of request — in practice, one of your controller methods. "Handler" is just the general word for "the thing that handles this request."
  • A handler mapping answers the question "given this request, which handler should run?" It holds the routing knowledge that used to live in the container's config.
  • A handler adapter knows how to actually invoke a given handler — how to call it and collect its result. Different styles of handler need different adapters, so this indirection keeps the DispatcherServlet from caring about the details.
  • A view resolver comes in when a handler returns the name of a page to render rather than raw data; it turns that name into something that can produce the final HTML.

Each of these is a Spring bean — an object Spring creates and manages inside its application context — so the DispatcherServlet simply asks the context for them. Hold these four names loosely for now; the next lessons take the mapping and adapter apart in detail. Here we just need to watch them cooperate.

Walking one request through the DispatcherServlet

Now we can trace what happens the moment a request arrives. Say a browser sends GET /users/42.

First, the container hands it over. Tomcat accepts the connection, parses the bytes into an HttpServletRequest, sees that /users/42 matches the / mapping, and calls the DispatcherServlet — exactly like calling doGet on any servlet.

Second, the DispatcherServlet asks: who handles this? It walks through its handler mappings, passing them the request, until one reports a match:

// Conceptually, inside the DispatcherServlet:
HandlerExecutionChain chain = null;
for (HandlerMapping mapping : this.handlerMappings) {
    chain = mapping.getHandler(request);   // "do you own /users/42?"
    if (chain != null) break;              // first match wins
}
Enter fullscreen mode Exit fullscreen mode

The result is the handler for /users/42 — your controller method — bundled with any interceptors that should run around it. If no mapping claims the request, this is the exact point where you get the familiar 404.

Third, it finds the right way to call that handler. The DispatcherServlet does not call your method directly. It looks for a handler adapter that understands this handler type:

// Pick the adapter that knows how to invoke this particular handler.
HandlerAdapter adapter = getHandlerAdapter(chain.getHandler());
Enter fullscreen mode Exit fullscreen mode

Fourth, the adapter invokes your code. The adapter reads the request, prepares whatever your method needs as arguments, calls it, and captures what it returns. Your controller method finally runs here — this is the one moment your own logic executes.

Fifth, the DispatcherServlet handles the result. What comes back is normalized into a single result object the DispatcherServlet knows how to process:

ModelAndView mv = adapter.handle(request, response, chain.getHandler());
Enter fullscreen mode Exit fullscreen mode

If your handler produced data to send straight back (the typical REST case), the response has effectively already been written and the DispatcherServlet is nearly done. If instead it returned the name of a page, the DispatcherServlet calls a view resolver to turn that name into a renderable view and asks it to write the HTML into the response.

Finally, the response goes back down. The finished HttpServletResponse returns to the container, which serializes it onto the socket and back to the browser. The DispatcherServlet's work for this request is over — until the next one arrives and the whole cycle repeats.

Why this shape is worth the indirection

Notice what the single front door buys you. Because every request passes through the DispatcherServlet before reaching any handler, there is now one obvious place to attach behavior that should apply to all of them — request logging, timing, security checks. You add it once, not in fifty servlets.

And because the DispatcherServlet only orchestrates — delegating the "which handler" question to mappings and the "how to call it" question to adapters — your controller code stays clean. You write a method that takes an id and returns a user. You never touch the socket, the thread, the parsing, or the routing table. All of that lives in machinery you did not have to build.

A couple of traps the design creates

The single-servlet setup has sharp edges worth knowing.

The first is a mapping surprise. Because the DispatcherServlet is mapped to /, it wants to handle everything, including requests for static files like /style.css. Spring has sensible defaults for serving static resources, but when a URL unexpectedly returns a 404 or the wrong content, the cause is often a collision between your handler mappings and static-resource handling — both living under that one broad mapping.

The second is about threads. The container runs each request on a thread from its pool, and the same DispatcherServlet instance serves them all at once. That is fine, because the DispatcherServlet holds no per-request state. But it means anything you hang onto across a request — a field on a shared bean, a ThreadLocal you forget to clear — is shared across concurrent requests too. The front controller is a single object handling many requests in parallel, and code downstream of it has to respect that.

Where this leaves us

So the picture is this: the servlet container owns the network and hands each request to one servlet. That servlet is the DispatcherServlet — Spring's front controller — and it does not answer requests itself. It asks a handler mapping which piece of your code should run, asks a handler adapter how to run it, lets your controller method do the real work, and then turns the result into a response.

We deliberately kept the mapping and the adapter at arm's length here, treating them as helpers that just "know" the answer. That is exactly where the next step goes: how a handler mapping actually decides that /users/42 belongs to your method, and how the adapter builds the arguments to call it — the request lifecycle, one stage at a time.

Source: dev.to

arrow_back Back to Tutorials