Adetayo Akinsanya unkletayo.dev

The Front Controller Pattern: How DispatcherServlet Routes HTTP Requests

Understanding Servlet API primitives, DispatcherServlet initialization, and the doDispatch execution loop

Part 11 in Series — Catch up on the previous article: Spring Boot Starters & Embedded Web Servers: How Tomcat Runs Inside an Executable JAR (Part 10) before diving into this post.

Why You Need This in Real Life

Building a multi-tenant SaaS application in raw Java Servlets without a framework forces every endpoint to extend HttpServlet: OrderServlet, UserServlet, PaymentServlet. Each servlet must manually duplicate security checks, character encoding filters, JSON parsing, database connection opening/closing, and exception formatting:

public class OrderServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // Duplicate Security Check
        if (!Authenticator.isAuthenticated(req)) {
            resp.sendError(401, "Unauthorized");
            return;
        }
        // Duplicate UTF-8 Encoding
        req.setCharacterEncoding("UTF-8");
        // Duplicate JSON Parsing & Exception Handling...
    }
}

When compliance requirements force you to append an audit tracking header to every API response, you must edit 40 individual Servlets.

The Front Controller Pattern solves this maintenance nightmare by providing a single centralized HTTP entry point that handles cross-cutting concerns (authentication, locale resolution, logging, error handling) before delegating to specialized handler methods. In Spring MVC, that central engine is DispatcherServlet.


Part 1: Evolution of the Servlet API & Front Controller Pattern

Traditional Servlet Architecture vs Front Controller Architecture

In standard Java EE applications, the servlet container (Tomcat) inspects URL patterns and dispatches incoming sockets directly to matching Servlets registered in web.xml:

Traditional Servlet Mapping:
GET /orders  -----> OrderServlet.doGet()
POST /users  -----> UserServlet.doPost()
GET /items   -----> ItemServlet.doGet()

Front Controller Pattern (DispatcherServlet):
GET /orders  ---\
POST /users  ----> [ DispatcherServlet ] ---> Centralized Interceptors/Filters ---> Handler Method
GET /items   ---/  (Front Controller)

By mapping DispatcherServlet to /* or /, Spring MVC captures every incoming HTTP request, transforming raw HTTP stream data into high-level Java method invocations.


Part 2: Dissecting DispatcherServlet Inheritance Hierarchy

DispatcherServlet is not a standalone framework magic class; it is a standard jakarta.servlet.http.HttpServlet descendant:

               jakarta.servlet.Servlet (Interface)
                          |
             jakarta.servlet.GenericServlet
                          |
             jakarta.servlet.http.HttpServlet
                          |
             org.springframework.web.servlet.HttpServletBean
                          |
             org.springframework.web.servlet.FrameworkServlet
                          |
             org.springframework.web.servlet.DispatcherServlet

Hierarchy Responsibilities

  1. HttpServletBean: Wraps servlet initialization parameters from ServletConfig into Spring BeanWrapper properties.
  2. FrameworkServlet: Integrates the Servlet with a Spring ApplicationContext. Overrides standard doGet(), doPost(), doPut(), doDelete() methods from HttpServlet, redirecting them all to a single central method: processRequest(HttpServletRequest request, HttpServletResponse response).
  3. DispatcherServlet: Receives processRequest() calls and delegates execution to doDispatch(request, response).

Part 3: DispatcherServlet Strategy Initialization

When Tomcat initializes DispatcherServlet, Spring executes onRefresh(ApplicationContext context) to resolve and cache nine strategic infrastructure components.

// Inside DispatcherServlet.java
@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);       // Maps URIs to HandlerMethods
    initHandlerAdapters(context);       // Executes HandlerMethods with argument resolution
    initHandlerExceptionResolvers(context); // Handles exceptions
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);         // Resolves HTML/JSP views
    initFlashMapManager(context);
}

If custom beans for these strategies are not present in your ApplicationContext, DispatcherServlet loads default implementations specified in DispatcherServlet.properties on the classpath (e.g., RequestMappingHandlerMapping, RequestMappingHandlerAdapter).


Part 4: The Heart of Spring MVC: doDispatch() Execution Loop

Every HTTP request routed through Spring MVC is processed by doDispatch(). Below is a trace of the internal execution loop:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            // 1. Check for Multipart (File upload) request
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // 2. Determine handler (Controller method) for current request
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // 3. Determine handler adapter for handler
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            // 4. Apply preHandle() on registered HandlerInterceptors
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return; // An interceptor rejected the request (e.g., Auth failure)
            }

            // 5. Actually invoke the handler (Controller method execution)
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            applyDefaultViewName(processedRequest, mv);

            // 6. Apply postHandle() on registered HandlerInterceptors
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            dispatchException = new ServletException("Handler dispatch failed", err);
        }

        // 7. Process result (Render view or handle exception)
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        }
        else {
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }
}
+-----------------------------------------------------------------------------+
|                        doDispatch Execution Pipeline                        |
|                                                                             |
|  Incoming HttpServletRequest                                                |
|        |                                                                    |
|        v                                                                    |
|  1. getHandler(request) ---------> Finds HandlerExecutionChain              |
|        |                           (HandlerMethod + HandlerInterceptors)    |
|        v                                                                    |
|  2. applyPreHandle() ------------> Executes interceptor.preHandle()         |
|        |                           (Returns false -> abort request)         |
|        v                                                                    |
|  3. HandlerAdapter.handle() -----> Resolves args & executes @Controller     |
|        |                                                                    |
|        v                                                                    |
|  4. applyPostHandle() -----------> Executes interceptor.postHandle()        |
|        |                                                                    |
|        v                                                                    |
|  5. processDispatchResult() -----> Resolves View or @ExceptionHandler       |
|        |                                                                    |
|        v                                                                    |
|  6. triggerAfterCompletion() ----> Executes interceptor.afterCompletion()   |
+-----------------------------------------------------------------------------+

Part 5: Edge Cases & Real-World Gotchas

Gotcha 1: Blocking Interceptor preHandle() Cleanups

When HandlerInterceptor.preHandle() returns false, doDispatch() aborts execution immediately without calling postHandle(). However, interceptors that executed successfully prior to the failing interceptor must still have their afterCompletion() method called to prevent ThreadLocal memory leaks (e.g., clearing security context or tenant ID).

// Inside HandlerExecutionChain.java
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    for (int i = 0; i < this.interceptorList.size(); i++) {
        HandlerInterceptor interceptor = this.interceptorList.get(i);
        if (!interceptor.preHandle(request, response, this.handler)) {
            // Trigger afterCompletion ONLY for interceptors that already passed preHandle
            triggerAfterCompletion(request, response, null);
            return false;
        }
        this.interceptorIndex = i; // Track last successful interceptor index
    }
    return true;
}

Gotcha 2: Async Request Processing Thread Hand-Off

When a controller returns a CompletableFuture or DeferredResult, Tomcat’s worker thread is released back to the server thread pool immediately while asynchronous processing occurs on a background thread. When the async computation completes, the container performs a Servlet Dispatch Re-entry (AsyncContext.dispatch()), causing doDispatch() to execute a second time to render the HTTP response.


Next Steps

Now that we understand DispatcherServlet and the doDispatch() execution loop, we will examine how Spring MVC maps URIs to controller methods via HandlerMapping, resolves method arguments using HandlerMethodArgumentResolver, and serializes HTTP responses with HttpMessageConverter.

References & Further Reading

  1. Spring.io. Spring Framework Web Servlet Technology (DispatcherServlet, HandlerMapping). Spring Docs.
  2. Eclipse Foundation. Jakarta Servlet Specification 6.0. Jakarta EE Docs.
  3. Walls, C. (2022). Spring in Action (6th Edition) — Chapter 2: Developing Web Applications. Manning.

Up Next in Series →

Part 12: Spring MVC Request Lifecycle: HandlerMappings, HandlerAdapters, and MessageConverters

Continue to Part 12 →