# The Servlet API From Zero

> Learn the foundation under every Java web framework: what a servlet is, the servlet container and its thread-per-request lifecycle, handling HTTP by hand with HttpServlet, URL mapping and the front-controller pattern, filters and the filter chain, and sessions. Spring's DispatcherServlet, JAX-RS, and middleware all live on top of this - see it bare.


---

# The Servlet API From Zero

Underneath every Java web framework you'll ever use - Spring MVC, Jakarta EE's JAX-RS, even Quarkus on
the JVM - there's one ancient, stable foundation: the **Servlet API**. Spring's `DispatcherServlet` is,
as the name says, a *servlet*. JAX-RS runs on servlets. Every "middleware" you've heard of is a servlet
**filter** underneath. This is the bedrock the whole Java web world is built on, and almost nobody learns
it directly anymore - which is exactly why the frameworks feel like magic.

This is a **roots guide**. You'll rarely write raw servlets in a real job (the frameworks exist for good
reasons), but understanding them turns a dozen framework concepts from magic into mechanism: routing,
middleware, the request lifecycle, why your controllers must be thread-safe, how sessions work. We build
the mental model bare-metal first - an HTTP request arriving, a container handing it to your code, a
response going back - and then you'll recognize that exact shape inside every framework you touch.

> 📝 This assumes **Java** (classes, interfaces, inheritance) and a basic grasp of **HTTP**. If HTTP is
> fuzzy, read [HTTP, Explained](/guides/http-explained) first. This guide is the deepest "kill the magic"
> root under [Spring](/guides/spring-framework-from-zero) and [Jakarta EE](/guides/jakarta-ee-from-zero) - 
> most valuable *after* you've used a framework and want to see what's beneath it.

## How to read this

Read in order - it builds from a single bare servlet up to the front-controller pattern that frameworks
generalize. Short and foundational. Phases carry difficulty badges.

## The phases

1. **[What a Servlet Is](01-what-a-servlet-is.md)** 🟢 - the foundational unit of Java web: an object that handles HTTP requests, and the container that runs it.
2. **[The Servlet Container & Lifecycle](02-the-servlet-container-and-lifecycle.md)** 🟡 - init/service/destroy, one instance serving many threads, and why that demands thread-safety.
3. **[Handling Requests with HttpServlet](03-handling-requests.md)** 🟢 - `doGet`/`doPost`, reading the request, writing the response, by hand.
4. **[Mapping & the Front-Controller Pattern](04-mapping-and-the-front-controller.md)** 🟡 - URL mapping, and the one-servlet-routes-everything pattern that *is* DispatcherServlet's secret.
5. **[Filters & the Chain](05-filters-and-the-chain.md)** 🟡 - intercepting requests before/after your servlet - the root of all "middleware."
6. **[Sessions & State](06-sessions-and-state.md)** 🟡 - `HttpSession`, cookies, and how stateful behavior is built on a stateless protocol.
7. **[From Servlets to Frameworks](07-from-servlets-to-frameworks.md)** 🟢 - see the servlet inside Spring MVC, JAX-RS, and middleware; where to go next.

> Once you've seen the Servlet API bare, "a framework" reads as "conveniences over a servlet, a front
> controller, and a filter chain." The magic was always this.


---

# What a Servlet Is

Every Java web app you'll ever touch - Spring Boot, an old-school JSP site, a JAX-RS REST API, the
internal tool nobody remembers writing - is standing on the same foundation. Dig under the annotations and
the auto-configuration and the dependency injection, and you eventually hit a plain Java object whose whole
job is: take an HTTP request, hand back an HTTP response. That object is a **servlet**, and this guide is
about the bedrock it sits on.

This is the roots guide. You'll rarely write one of these by hand at work - frameworks exist precisely so
you don't have to - but every framework is a convenience layer over the thing you're about to meet.

## The foundation: what a servlet actually is

Let's name the thing before we do anything with it.

📝 **Servlet** - a Java object that handles an HTTP request and produces an HTTP response. It's the base
unit of server-side Java web programming, defined by the **Servlet API** (the package `jakarta.servlet.*`,
called `javax.servlet.*` in older code - same idea, renamed when Java EE became Jakarta EE). A servlet has
a couple of methods on it; you fill them in; something calls them when a request arrives.

That's genuinely the whole concept. A servlet is not a server, not a framework, not a protocol - it's *one
object that answers requests*. The reason it matters out of all proportion to how small it is: **every Java
web framework runs on servlets.** Spring MVC, Jakarta Faces, JAX-RS, Struts - pick any of them, scrape off
the surface, and there's a servlet underneath doing the actual request-handling. So this isn't a museum
piece. It's the layer the entire ecosystem is built on, and understanding it is what makes the rest
legible.

> 💡 **Key point.** A servlet is the smallest unit of "code that answers HTTP" in Java. Frameworks are
> elaborate, helpful ways of arranging servlets. When something in Spring feels like sorcery, the plain
> answer is almost always "a servlet is doing it."

## The container runs it - you don't

Here's the part that trips people up first: you never start a servlet yourself. There's no `main` method
that boots it up, no line where you call your own servlet. Something else owns that job.

📝 **Servlet container** (also called a **servlet engine** or **web container**) - a program that accepts
incoming TCP connections, parses the raw HTTP, figures out which servlet should handle the request, calls
your servlet's method, and sends your response back over the wire. Tomcat, Jetty, and Undertow are the
common ones. The container is the thing that's actually *running*; your servlet is a guest it invites in
when a request shows up.

Picture the division of labor:

```mermaid
flowchart LR
  B[Browser] -->|raw HTTP over TCP| C[Servlet container]
  C -->|parses request, picks servlet| S[Your servlet]
  S -->|writes response| C
  C -->|HTTP response| B
```

*What just happened:* the browser opens a connection and sends bytes. The **container** does every piece of
unglamorous plumbing - accepting the socket, reading the bytes, parsing them into a tidy request object,
deciding your servlet is the right handler, and afterward serializing your response back into HTTP and
shipping it. Your servlet sits in the middle and does the one interesting part: looks at the request,
decides what to send back.

💡 **The container does the HTTP grunt work; you write the handler.** This is the deal the Servlet API
strikes with you. You don't parse headers, manage sockets, or speak HTTP/1.1 by hand - the container hands
you a parsed request and a response to fill in. In exchange, you write your logic to *its* shape: a class
with the right methods, that it knows how to call. (If "the framework owns the loop and calls your code"
rings a bell, it should - it's the inversion of control from
[/guides/what-a-framework-even-is](/guides/what-a-framework-even-is), here at its root.)

## What one actually looks like

Enough description - here's a servlet. This is a taste, not a tutorial; the line-by-line detail comes in
Phase 3. For now, read it for shape, not mastery.

```java
import java.io.IOException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        response.setContentType("text/plain");
        response.getWriter().write("Hello from a servlet");
    }
}
```

*What just happened:* `HelloServlet` extends `HttpServlet` - the Servlet API's base class for handling
HTTP. We overrode `doGet`, the method the container calls when a `GET` request arrives for this servlet.
The container handed us two objects: `request` (the parsed incoming request) and `response` (where we write
what to send back). We set the content type and wrote a string to the response's writer. That's a complete,
working servlet - no `main`, no socket code, no HTTP parsing. The container supplies the request and ships
the response; we filled in the middle.

Now picture the exchange it powers. A browser sends:

```http
GET /hello HTTP/1.1
Host: example.com
```

and the container, after running our `doGet`, sends back:

```http
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 21

Hello from a servlet
```

*What just happened:* the request line `GET /hello` told the container two things - the method (`GET`) and
the path (`/hello`). It routed that to `HelloServlet`, called `doGet`, and turned what we wrote into a
proper HTTP response: a `200 OK` status, the `Content-Type` header we set, a `Content-Length` the container
computed for us, and the body. We wrote one line of output; the container handled the rest of the protocol.
(If status codes and headers are fuzzy, [/guides/http-explained](/guides/http-explained) is the companion
read.)

## Where this sits under your frameworks

Here's the reveal that justifies the whole guide. That small `HttpServlet` you just saw? It's not a relic
you'll skip past on the way to "real" frameworks. It *is* what the real frameworks are made of.

- **Spring MVC** routes every request through a single servlet called `DispatcherServlet`. That's a real
  servlet - extends `HttpServlet`, has a `doGet`/`doPost`, the whole thing. When you write a Spring
  `@Controller`, Spring's `DispatcherServlet` receives the request from the container first, then dispatches
  it to your controller method. The annotations are a routing layer *on top of one servlet*.
- **JAX-RS** (Jakarta REST) - your `@Path`-annotated resource classes are reached because a servlet at the
  front catches requests and dispatches them to the right resource method.
- **Middleware** - that cross-cutting "runs before and after every request" layer every framework has? In
  the Servlet API it's a built-in concept called a **`Filter`**. Frameworks wrap it, rename it, decorate it
 - but a Spring filter chain is, underneath, Servlet API `Filter`s.

💡 **Frameworks are conveniences over this layer.** Routing, middleware, request parsing, dependency
injection - the framework adds ergonomics, but the request still enters through the container and lands on a
servlet. When you learn [/guides/spring-boot-from-zero](/guides/spring-boot-from-zero) and read that the
embedded Tomcat "just serves your app," now you know precisely what that means: Tomcat is the container,
Spring's `DispatcherServlet` is the servlet, and your controllers are what it dispatches to.

## Why bother learning it

⚠️ **You will rarely write a raw servlet in a real job - and that's fine.** Frameworks exist for good
reasons: they spare you boilerplate, give you routing and DI and validation, and encode years of hard-won
defaults. Reaching for raw servlets when Spring would do is usually a mistake, not a badge of honor. This
guide is *not* arguing you should write servlets by hand.

It's arguing something more useful: knowing this layer is what makes the layers above it stop being magic.
Once you've seen the servlet underneath, a pile of mysteries resolve at once - 

- **Routing** is "which servlet (or which method behind the dispatcher servlet) handles this path."
- **Middleware** is the Servlet API's `Filter` chain.
- **The request lifecycle** - where parsing happens, where your code runs, where the response gets sent - is
  the container's job, which you can now reason about.
- **Thread-safety surprises** (the bug where two users see each other's data) come straight from how the
  container reuses one servlet instance across many concurrent requests - a Phase 2 topic that's terrifying
  until you understand the container, and obvious afterward.

That's the payoff. Next we follow the request all the way in: how the container creates your servlet, when
it calls your methods, and why "one servlet, many threads" is the single most important thing to understand
before you trust a servlet with real traffic.

## Recap

1. A **servlet** is a Java object that handles an HTTP request and produces a response - the base unit of
   server-side Java web programming, defined by the Servlet API (`jakarta.servlet.*`, formerly
   `javax.servlet.*`).
2. **Every Java web framework runs on servlets.** Spring MVC, JAX-RS, and the rest are convenience layers
   over this foundation - which is why understanding it makes them legible.
3. You don't run a servlet yourself. A **servlet container** (Tomcat, Jetty, Undertow) accepts connections,
   parses HTTP, calls your servlet, and sends the response back. The container does the grunt work; you
   write the handler.
4. A servlet `extends HttpServlet` and overrides methods like `doGet`. The container hands it a parsed
   `request` and a `response` to fill in - no `main`, no socket code, no HTTP parsing on your part.
5. Spring's `DispatcherServlet` *is* a servlet; JAX-RS is dispatched by one; "middleware" is the Servlet
   API's `Filter`. Frameworks rename and wrap these, but the shape underneath is the same.
6. ⚠️ You'll rarely write raw servlets at work - but knowing this layer is what turns routing, middleware,
   the request lifecycle, and thread-safety from magic into mechanism.

## Quick check

Three questions on the ideas that have to stick before Phase 2:

```quiz
[
  {
    "q": "In plain terms, what is a servlet?",
    "choices": [
      "A Java object that handles an HTTP request and produces a response - the base unit of server-side Java web programming",
      "A web server you download and install, like Tomcat",
      "A Spring annotation that marks a class as a controller",
      "The HTTP protocol itself, implemented in Java"
    ],
    "answer": 0,
    "explain": "A servlet is just an object with request-handling methods, defined by the Servlet API. Tomcat is the container that runs servlets; Spring annotations are a layer on top; HTTP is the protocol the container speaks for you."
  },
  {
    "q": "Who actually accepts the TCP connection, parses the raw HTTP, and calls your servlet's method?",
    "choices": [
      "The servlet container (Tomcat, Jetty, Undertow)",
      "Your servlet, in its own main method",
      "The browser that sent the request",
      "The Servlet API package itself, at compile time"
    ],
    "answer": 0,
    "explain": "The container owns the running process and all the HTTP plumbing: it accepts the socket, parses the request, picks the right servlet, calls it, and ships the response. Your servlet has no main and never touches the socket."
  },
  {
    "q": "How does Spring MVC relate to the Servlet API?",
    "choices": [
      "Spring's DispatcherServlet IS a servlet; it receives requests from the container and dispatches them to your @Controller methods",
      "Spring replaces the Servlet API entirely with its own protocol",
      "Spring only works without a servlet container",
      "Spring controllers are themselves separate servlet containers"
    ],
    "answer": 0,
    "explain": "DispatcherServlet extends HttpServlet - a real servlet. The container hands it the request first, and it routes to your annotated controller methods. Frameworks are conveniences built over the servlet layer, not replacements for it."
  }
]
```


---

# The Servlet Container & Lifecycle

In Phase 1 you wrote a servlet - an object with methods that handle HTTP. But you never called those
methods. You never wrote `new MyServlet()`, never wired it to a socket, never spun up a thread to run
it. So who does?

📝 **The container owns your servlet's life, and it runs one copy of it across many threads at once.**
Tomcat, Jetty, Undertow - these are the *containers*. You hand them a class; they decide when to create it,
when to call it, when to throw it away, and - the part that bites people - they call it from *many threads
simultaneously, on a single shared instance*. Get that one sentence into your bones and the rest of this
phase, plus half of why Spring controllers look the way they do, falls out of it.

Let's take it in three moves: the lifecycle (when your code runs), the one-instance fact (how many copies
exist), and the thread-safety consequence (the lesson that actually matters in production).

## The lifecycle: init → service → destroy

📝 **A servlet's life has exactly three phases, and the container drives all three.** When the
application starts (or on the first request, depending on config), the container creates your servlet and
calls `init()` **once**. From then on, every incoming HTTP request makes the container call `service()` - 
which routes to `doGet`, `doPost`, and friends (Phase 3). When the application shuts down, the container
calls `destroy()` **once**, and the instance is gone.

```mermaid
flowchart TD
  A[App starts] --> B["container creates ONE instance"]
  B --> C["init() - called once"]
  C --> D["service() - called for EVERY request"]
  D --> D
  D --> E["destroy() - called once"]
  E --> F[App stops]
```

The shape that matters: `init` and `destroy` are *bookends* - once each, at the edges of the servlet's
life. `service` is the *hot loop* - called again and again, once per request, for the entire time the app
is up.

You override `init` and `destroy` when you have setup and cleanup that should happen one time, not on
every request - opening a database connection pool, loading a config file, starting a background client.

```java
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;

public class ReportServlet extends HttpServlet {

    private DataSource pool;   // expensive to build - do it once

    @Override
    public void init() throws ServletException {
        // runs ONCE, when the container creates this servlet
        this.pool = buildConnectionPool();
        System.out.println("ReportServlet initialised");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        // runs on EVERY GET - keep it about handling this one request
        resp.getWriter().write("rows: " + queryRowCount(pool));
    }

    @Override
    public void destroy() {
        // runs ONCE, at shutdown - release what init() acquired
        pool.close();
        System.out.println("ReportServlet destroyed");
    }
}
```

*What just happened:* The container called `init()` a single time and we used that one shot to build an
expensive connection pool, stashing it in a field. Every `doGet` after that reuses the same pool instead
of rebuilding it per request. At shutdown the container called `destroy()` once, where we closed the
pool. ⚠️ Notice the field `pool` - it survives across every request because there's only one servlet
instance holding it. That's convenient here (the pool is shared and read-only after setup) and a
*trap* in a moment, when the field is mutable. Hold that thought.

## ONE instance, MANY requests

Here's the fact that surprises almost everyone the first time, and it's the linchpin of the phase:

📝 **The container creates exactly ONE instance of your servlet and reuses it for every single request.**
It does *not* do `new ReportServlet()` per request. It is, in practice, a **singleton** - one object,
living for the whole lifetime of the app, handling thousands or millions of requests.

This is why the connection pool above worked: `init()` ran once, on the one instance, and every request
saw the same pool. If the container made a fresh servlet per request, each would rebuild the pool - slow
and pointless. So the container's design is deliberate: build the handler once, keep it warm, route all
traffic through it.

That single decision is the *entire* reason this phase exists. One instance is cheap and fast. One
instance is also **shared** - and shared is where the trouble starts.

## Thread-per-request: many threads, one instance

A real server handles many requests at the same time. It can't process them one-at-a-time in a queue;
your users would wait in line. So:

📝 **The container keeps a pool of worker threads, and runs each incoming request on a thread pulled from
that pool - all of them calling `service()` on the same single servlet instance, concurrently.** Request
A is being handled by thread 1, request B by thread 2, request C by thread 3 - at the same instant, all
three threads executing the *same* `doGet` method on the *same* object.

```mermaid
flowchart TD
  R1[request A] --> T1[pool thread 1]
  R2[request B] --> T2[pool thread 2]
  R3[request C] --> T3[pool thread 3]
  T1 --> S["the ONE servlet instance - service()"]
  T2 --> S
  T3 --> S
```

If you read [Java's concurrency phase](/guides/java-from-zero), the alarm should already be ringing. This
is *exactly* the dangerous setup from that chapter: **multiple threads touching the same object's shared
state at the same time.** The "thread pool" here is the same `ExecutorService` idea - Tomcat's default is
a couple hundred worker threads - and the "shared mutable state" is any field on your servlet. The
container handles the threading *for* you, which is great, right up until you forget it's happening and
write to a field.

## The thread-safety consequence - the big lesson

⚠️ **Because one instance is shared across many threads, any mutable instance field on a servlet is a
data race.** This is the single most important thing to take from this phase. Let's see it break.

Imagine you want to count how many requests you've served, so you add a field and increment it:

```java
public class CounterServlet extends HttpServlet {

    private int hits = 0;   // DANGER: mutable field on a shared, multithreaded instance

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        hits++;                                  // read-add-write - NOT atomic
        resp.getWriter().write("hit #" + hits);
    }
}
```

```console
# 1000 concurrent requests fired at this servlet:
expected final count: 1000
actual final count:   948
```

*What just happened:* Exactly the broken counter from the Java concurrency phase, now wearing a web
server's clothes. `hits++` is three steps - **read** the value, **add** one, **write** it back - and
because every request runs on its own thread against the *one* shared `CounterServlet`, two threads can
read `500` at the same instant, both compute `501`, and both write `501`. Two requests, one increment;
the lost updates pile up and you land at `948` instead of `1000`. ⚠️ It's *intermittent* - under light
traffic it might look fine for weeks, then corrupt under load. That's the signature of a race condition,
and it's the worst kind of bug to chase in production.

The fix is not to sprinkle locks everywhere. The fix is to **not keep request state in fields at all**:

```java
public class GreetServlet extends HttpServlet {

    // no mutable fields - nothing shared between requests

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        String name = req.getParameter("name");   // local variable - per request, per thread
        String greeting = "Hello, " + name;       // also local - no sharing possible
        resp.getWriter().write(greeting);
    }
}
```

*What just happened:* Every piece of per-request data - `name`, `greeting` - is a **local variable**.
Local variables live on each thread's own stack, so thread 1's `name` and thread 2's `name` are entirely
separate; there is nothing shared to corrupt. The servlet itself holds no changing state, so it's safe to
hammer with a thousand concurrent threads. 💡 The rule that drops out of this: **keep servlets
stateless.** Per-request data goes in local variables and the request object; anything genuinely shared
(like that read-only connection pool, set up once in `init`) is fine, and anything that *must* be mutable
and shared needs a real concurrency tool - `AtomicInteger`, a `ConcurrentHashMap`, a lock - never a plain
field.

💡 **And here's the payoff that reaches far beyond raw servlets.** Ever wondered *why* Spring controllers
are singletons that you're told to keep stateless? Why "don't store request data in controller fields" is
drilled into every Spring tutorial? This is why. A Spring `@RestController` is, underneath, handled by one
shared object inside the `DispatcherServlet` - which is itself a servlet, running on this exact
thread-per-request, one-instance model. The rule isn't a Spring quirk; it's the servlet container's
shared-instance reality, inherited. Learn it here, bare, and it stops being a memorized rule and becomes
something you actually understand.

## Container responsibilities - what you got for free

Step back and notice how much the container did that you never wrote. It managed the **thread pool**,
accepted the **network connections** and parsed the raw HTTP, created and **deployed** your servlet,
called the **lifecycle** methods at the right moments, and (you'll see in Phase 5) runs the **filters and
listeners** around your code too. You wrote one method that handles one request; the container handled
concurrency, sockets, parsing, and lifetime.

💡 **This is the original "inversion of control" in Java web.** You don't call the framework - the
*framework calls you*. Your code doesn't run a loop pulling requests off a socket; you hand the container
a class and it invokes your methods when requests arrive. That "you write the handler, the runtime drives
it" inversion is the deepest pattern in the whole Java web stack - and every framework on top of servlets
(Spring, JAX-RS, all of it) is built on this same hand-it-over-and-let-it-call-you arrangement.

## Recap

1. **The container owns the lifecycle: `init` → `service` → `destroy`.** `init()` runs once at startup
   (do expensive setup there), `service()` runs for *every* request (routing to `doGet`/`doPost`),
   `destroy()` runs once at shutdown (clean up what `init` acquired).
2. **One instance, reused for every request.** The container creates a *single* servlet instance - an
   effective singleton - not a new one per request. That's why setup in `init` is worth doing once.
3. **Thread-per-request on that one instance.** Each request runs on a worker thread from the container's
   pool, and they all call `service()` on the *same shared object* concurrently - the classic
   shared-mutable-state danger from [Java concurrency](/guides/java-from-zero).
4. **Mutable instance fields are a data race.** A plain `int hits` counter loses updates under load
   because `hits++` isn't atomic and the field is shared across threads. ⚠️ Intermittent, load-dependent,
   miserable to debug.
5. **Keep servlets stateless** - use local variables and the request object for per-request data. 💡 This
   is *exactly* why Spring controllers are stateless singletons: they inherit the servlet container's
   one-instance, multithreaded model.
6. **The container handles concurrency, networking, parsing, and lifetime for you** - the original
   *inversion of control*: you write the handler, the runtime calls it.

## Quick check

Three questions on the model that explains half of Java web framework behaviour:

```quiz
[
  {
    "q": "How many instances of a given servlet class does the container create, and how does it serve concurrent requests?",
    "choices": [
      "One instance, reused for every request; concurrent requests each run on their own thread from a pool, all calling service() on that one shared instance",
      "A new instance per request, each on its own thread, so no state is ever shared",
      "One instance, and requests are queued and processed strictly one at a time",
      "One instance per worker thread, so each thread has a private copy"
    ],
    "answer": 0,
    "explain": "The container makes a single servlet instance (effectively a singleton) and runs each request on a pooled thread, all hitting the same instance concurrently. That shared instance is the whole reason thread-safety matters."
  },
  {
    "q": "A servlet has a `private int hits = 0;` field incremented with `hits++` in doGet. Under heavy concurrent traffic the count comes out too low. Why?",
    "choices": [
      "`hits++` is read-add-write (not atomic), and because the one servlet instance is shared across request threads, two threads can read the same value and one increment is lost - a race condition",
      "The container resets the field between requests",
      "Each request gets its own copy of the field, so they never add up",
      "Integers in servlets silently overflow under load"
    ],
    "answer": 0,
    "explain": "One shared instance + many threads + a non-atomic read-add-write = lost updates. The fix is to keep servlets stateless (local variables, the request object) or use a real concurrency tool like AtomicInteger."
  },
  {
    "q": "Why are Spring controllers conventionally kept stateless (no request data in fields)?",
    "choices": [
      "Because they run on the same servlet model: a single shared instance handling requests across many threads, so mutable fields would be a data race - exactly as in a raw servlet",
      "Because Spring creates a brand-new controller per request and fields are wiped anyway",
      "It's purely a style preference with no technical basis",
      "Because Spring controllers can't legally declare fields at all"
    ],
    "answer": 0,
    "explain": "A Spring @RestController is handled by one shared object inside the DispatcherServlet - itself a servlet on the thread-per-request, one-instance model. The 'keep it stateless' rule is the servlet container's shared-instance reality inherited, not a Spring quirk."
  }
]
```


---

# Handling Requests with HttpServlet

In Phase 2 you saw the container create one instance of your servlet and feed every request through a
single `service` method. That `service` method is where the real work happens - but you almost never
override it directly. Instead you extend `HttpServlet`, which has already done the tedious part: it looks
at the HTTP method on the incoming request and routes it to a method named after that verb.

📝 **The mental model for this whole phase:** an HTTP request is two things glued together - a *method*
(GET, POST, ...) and a *payload* (the URL, headers, and body). `HttpServlet` splits those apart for you.
The method picks which of your functions runs; the payload arrives as a `HttpServletRequest` object you
read from. You write your answer into a `HttpServletResponse` object. Read request, write response. That's
the entire job. Everything a web framework does is a fancier version of exactly this.

> If "method," "header," "status code," and "body" feel fuzzy, spend ten minutes in
> [HTTP & JSON: the API Building Blocks](/guides/http-and-json-api-basics) first - this phase assumes you
> can read a raw request and response.

## HttpServlet & the doXxx methods

`HttpServlet` gives you one method per HTTP verb. You override the one(s) you care about; the container
calls the right one based on the request line:

| HTTP request | Method called |
|--------------|---------------|
| `GET /users` | `doGet` |
| `POST /users` | `doPost` |
| `PUT /users/7` | `doPut` |
| `DELETE /users/7` | `doDelete` |

Here's a servlet that handles both reading and creating - GET to list, POST to add:

```java
public class UserServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.setContentType("text/plain");
        resp.getWriter().write("Here is the list of users.");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.setStatus(201); // Created
        resp.getWriter().write("A new user was created.");
    }
}
```

*What just happened:* `HttpServlet`'s built-in `service` method inspected the request line. A `GET`
landed in `doGet`; a `POST` landed in `doPost`. You never wrote a single `if (method.equals("POST"))` -
inheritance did the dispatch. Any verb you *don't* override (say `DELETE`) gets a polite automatic `405
Method Not Allowed` from the parent class, which is exactly what you want.

💡 If you ever override a `doXxx` method, don't call `super.doGet(...)` unless you mean it - the parent's
default is to return that `405`, which will clobber your response.

## Reading the request (HttpServletRequest)

The `HttpServletRequest` is the whole incoming message turned into an object. The pieces you'll reach for
constantly:

| You want… | Call |
|-----------|------|
| A query string or form field | `req.getParameter("name")` |
| A request header | `req.getHeader("Content-Type")` |
| The path after the servlet's mapping | `req.getPathInfo()` |
| The raw body (for JSON) | `req.getReader()` or `req.getInputStream()` |

`getParameter` is the workhorse. It pulls from the query string for a GET and from a URL-encoded form
body for a POST - same call, the container figures out where to look:

```java
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws IOException {
    String name = req.getParameter("name");   // /greet?name=Ada  ->  "Ada"
    String accept = req.getHeader("Accept");   // e.g. "application/json"

    resp.setContentType("text/plain");
    resp.getWriter().write("Hello, " + (name == null ? "stranger" : name));
}
```

*What just happened:* the container parsed `?name=Ada` off the URL and handed you the value through
`getParameter`. Note it returns `null` when the param is absent - there's no exception, so you check for
it yourself. `getHeader` reads any header by name, case-insensitively.

For a JSON API, the data doesn't arrive as named params - it's a raw body you read as a stream of text:

```java
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws IOException {
    StringBuilder body = new StringBuilder();
    try (BufferedReader reader = req.getReader()) {
        String line;
        while ((line = reader.readLine()) != null) {
            body.append(line);
        }
    }
    // body now holds the raw JSON text, e.g. {"name":"Ada","role":"admin"}
    String json = body.toString();

    resp.setStatus(201);
    resp.setContentType("text/plain");
    resp.getWriter().write("Received " + json.length() + " bytes of JSON.");
}
```

*What just happened:* `getReader()` gave you the request body as character text, which you drained
line by line into a string. At this point you have raw JSON - a real app would hand that string to a
parser (more on that below). The `try`-with-resources block closes the reader for you.

⚠️ **Read the body once.** `getParameter` on a POST quietly *consumes* the form body to find its values,
and `getReader`/`getInputStream` consume the body too. You can't have both, and you can't read the stream
twice - the second read comes back empty. Decide up front: form params *or* raw body, not both, and read
the body a single time.

## Writing the response (HttpServletResponse)

The response object is your outgoing message, and you build it in a specific order: status and headers
*first*, body *last*. Once you start writing the body, the status line and headers have already been sent,
so setting them afterward does nothing.

| You want… | Call |
|-----------|------|
| Set the status code | `resp.setStatus(201)` |
| Set the content type | `resp.setContentType("application/json")` |
| Set any header | `resp.setHeader("Cache-Control", "no-store")` |
| Write the body | `resp.getWriter().write(...)` |

Here's a servlet returning JSON, assembled entirely by hand:

```java
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws IOException {
    resp.setStatus(200);
    resp.setContentType("application/json");
    resp.setCharacterEncoding("UTF-8");

    String json = "{\"id\":7,\"name\":\"Ada\",\"role\":\"admin\"}";
    resp.getWriter().write(json);
}
```

*What just happened:* you set the status, declared the content type so the client knows to parse it as
JSON, then wrote the body string through the writer. Notice you built the JSON by hand-concatenating a
string with escaped quotes - clumsy and error-prone, but it shows there's no magic. It's just text going
down a socket.

The wire result of that code looks like this:

```http
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8

{"id":7,"name":"Ada","role":"admin"}
```

💡 In real code you would never hand-build that string. You'd hand an object to a JSON library - Jackson
or Gson - and let it serialize:

```java
// What you'd actually do: let Jackson turn an object into JSON text
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(user);  // -> {"id":7,"name":"Ada",...}
resp.setContentType("application/json");
resp.getWriter().write(json);
```

*What just happened:* the `ObjectMapper` walked the fields of your `user` object and produced the JSON
text for you - same bytes as the hand-built string, none of the escaping. This is the one part of raw
servlet work that frameworks really do save you from. Seeing it bare once is the point; you won't do it
this way again.

## By hand vs the framework

Step back and look at what all that code actually did, in order:

1. The container picked `doGet` or `doPost` based on the HTTP method.
2. You pulled values out of the request - params, headers, body.
3. You ran your logic.
4. You serialized a result and wrote it back with a status code.

💡 That list **is** what a Spring controller does - the framework has just hidden each step behind an
annotation:

```java
// Spring MVC - the same four steps, annotated
@GetMapping("/users/{id}")          // step 1: route GET to this method
public User getUser(@PathVariable int id,        // step 2: bind from the request
                    @RequestParam String fields) {
    return userService.find(id);     // step 3 + 4: return an object; Spring serializes it
}
```

*What just happened:* `@GetMapping` is doing the `doGet`-style dispatch. `@PathVariable` and
`@RequestParam` are doing your `getParameter`/`getPathInfo` reads. Returning a `User` object instead of
writing a string is the framework calling Jackson and `getWriter().write(...)` for you. Same servlet
machinery underneath - `@GetMapping` is convenience over `doGet`, nothing more. The servlet is the
unglamorous truth beneath the annotations; once you've seen it, the annotations stop being magic and start
being shorthand.

## The request and response are per-call

Here's the thread-safety thread from Phase 2, finally tied off. Remember: the container keeps *one*
instance of your servlet and runs `doGet`/`doPost` on it from many threads at once. So how is the code
above safe?

💡 Because the `req` and `resp` objects are **created fresh by the container for every single request** and
passed in as parameters. Thread A's `doGet` gets thread A's request; thread B's `doGet` gets a completely
separate request object. The shared thing (the servlet instance) holds no per-request data; the
per-request things (the request and response) aren't shared. That's the whole trick.

```java
public class CounterServlet extends HttpServlet {

    private int hits = 0; // ⚠️ DANGER: shared across all threads

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        hits++;                                  // race condition!
        int callId = req.hashCode();             // safe: per-request object

        resp.getWriter().write("Hit number " + hits);
    }
}
```

*What just happened:* `hits` is an instance field on the one shared servlet, so two threads incrementing
it at once will trample each other and lose counts - a classic race. But anything you derive from `req`
is yours alone, because `req` was minted for this one call. The rule that falls out: **keep per-request
state in local variables and in the request object, never in servlet fields.** Method-local variables
live on each thread's own stack, so they can't collide.

This per-call request object is also what makes routing possible - the next phase reads `req.getPathInfo()`
to decide *which* handler should run, letting a single servlet dispatch to many. That's the
front-controller pattern, and it's where DispatcherServlet's secret lives.

## Recap

- `HttpServlet` routes each request to a `doXxx` method by HTTP verb - override `doGet`, `doPost`, etc.;
  unhandled verbs auto-return `405`.
- Read the request with `getParameter` (query + form), `getHeader`, `getPathInfo`, and `getReader`/
  `getInputStream` for a raw JSON body.
- You can read the body **once**: `getParameter` on a POST consumes the form body, and the input
  stream/reader can't be re-read.
- Build the response status and headers *before* the body: `setStatus`, `setContentType`, `setHeader`,
  then `getWriter().write(...)`.
- Hand-writing JSON is the raw truth; real apps let Jackson/Gson serialize - and `@GetMapping` /
  `@RequestParam` / returning an object is exactly these steps with annotations on top.
- The request and response are created per request, so they're safe to use even though the servlet
  instance is shared - keep state in locals, not fields.

## Quick check

```quiz
[
  {
    "q": "A POST request arrives and your servlet only overrides doGet. What happens?",
    "choices": ["doGet runs anyway", "The container returns 405 Method Not Allowed", "The request hangs forever"],
    "answer": 1,
    "explain": "HttpServlet's default doPost returns 405 Method Not Allowed, since you didn't override it."
  },
  {
    "q": "Why is it unsafe to call getParameter and then read the body with getReader on the same POST?",
    "choices": ["getParameter is slower", "getParameter can consume the form body, so the reader comes back empty", "getReader only works on GET requests"],
    "answer": 1,
    "explain": "Reading params on a POST can consume the body; the body can only be read once, so the later read finds nothing."
  },
  {
    "q": "One servlet instance serves many threads. What makes the request/response objects safe to use?",
    "choices": ["They are synchronized with locks", "They are created fresh by the container for each request and passed in", "They are stored in static fields"],
    "answer": 1,
    "explain": "The container mints a new request and response per call and passes them as parameters, so no two threads share them."
  }
]
```


---

# Mapping & the Front-Controller Pattern

In [Phase 3](03-handling-requests.md) you wrote a servlet that reads a request and writes a response. But we waved a hand over one thing: *how does the container know that `GET /hello` should reach your `HelloServlet` and not some other class?* That's the missing link, and chasing it all the way down lands you on the single most important pattern in Java web development - the one every framework you've ever used is quietly built on.

**The container keeps a routing table that maps URL patterns to servlets.** When a request arrives, it looks up the path, finds the matching servlet, and hands the request over. Everything in this phase is about who writes that table, and the moment you realize you can write a *clever* one yourself.

## URL mapping: how the container finds your servlet

📝 A servlet isn't reachable until it's *mapped* to a URL pattern. There are two ways to register that mapping, one modern and one classic.

The modern way is an annotation right on the servlet class:

```java
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    // doGet, doPost, etc.
}
```

*What just happened:* `@WebServlet("/products")` tells the container, at startup, "any request whose path is `/products` goes to this class." The container scans your classes for this annotation and builds its routing table from what it finds. No external file needed - the mapping lives next to the code it routes to.

The classic way is a `web.xml` deployment descriptor, an XML file the container reads at startup:

```xml
<servlet>
    <servlet-name>products</servlet-name>
    <servlet-class>com.example.ProductServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>products</servlet-name>
    <url-pattern>/products</url-pattern>
</servlet-mapping>
```

*What just happened:* Two linked blocks. `<servlet>` gives the class a name; `<servlet-mapping>` ties that name to a URL pattern. It's more verbose than the annotation and lives away from the code, but it does the exact same job - it's an entry in the same routing table. You'll still meet `web.xml` in older codebases, so it's worth recognizing.

💡 Both approaches feed the **same** container routing table. The annotation is just a more convenient way to write the entry. Pick one per servlet; don't map the same servlet both ways.

The URL pattern itself comes in a few flavors, and the differences matter:

- **Exact match** - `/products` matches *only* the path `/products`. Nothing else.
- **Path prefix** - `/products/*` matches `/products`, `/products/42`, `/products/42/reviews` - anything under that prefix.
- **Extension** - `*.do` matches any path ending in `.do`, like `/checkout.do`. (A relic of older frameworks, but you'll see it.)

⚠️ The catch-all pattern is `/*` - it matches **every** request, no matter the path. Hold that one in mind; it's the hinge this whole phase turns on.

## The naive approach: one servlet per URL

The obvious way to build an app is: one servlet per endpoint. Need two URLs? Write two servlets.

```java
@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.getWriter().write("Here are the products.");
    }
}

@WebServlet("/orders")
public class OrderServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.getWriter().write("Here are the orders.");
    }
}
```

*What just happened:* Two endpoints, two complete servlet classes, two mappings. It works, and for a two-page app it's perfectly fine.

⚠️ But picture a real app: products, orders, customers, login, search, an admin section - fifty endpoints. That's fifty servlet classes and fifty mappings to keep straight. Every cross-cutting concern (logging the request, checking authentication) has to be repeated or bolted on in each one. The mapping sprawl alone becomes a maintenance tax. This pattern doesn't scale - and the fix is the big idea of the whole guide.

## The front-controller pattern

📝 Here's the move. Instead of mapping many servlets to many URLs, map **one** servlet to `/*` - catch *everything* - and have that single servlet inspect the request path and dispatch to the right handler *itself*. One front door for the whole app, with the routing logic on the inside.

```mermaid
flowchart TD
  A[GET /products] --> FC[FrontControllerServlet  /*]
  B[GET /orders] --> FC
  C[GET /anything] --> FC
  FC --> R{inspect path}
  R -->|/products| H1[showProducts]
  R -->|/orders| H2[showOrders]
  R -->|else| H3[notFound 404]
```

Every request lands on the same servlet, which then reads the path and decides where it goes:

```java
@WebServlet("/*")
public class FrontControllerServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        String path = req.getPathInfo();   // the part after the context, e.g. "/products"

        if ("/products".equals(path)) {
            showProducts(resp);
        } else if ("/orders".equals(path)) {
            showOrders(resp);
        } else {
            resp.setStatus(404);
            resp.getWriter().write("Not found: " + path);
        }
    }

    private void showProducts(HttpServletResponse resp) throws IOException {
        resp.getWriter().write("Here are the products.");
    }

    private void showOrders(HttpServletResponse resp) throws IOException {
        resp.getWriter().write("Here are the orders.");
    }
}
```

*What just happened:* One servlet, mapped to `/*`, now owns every incoming GET. It pulls the path out of the request, runs it through an `if`/`else` chain, and calls the matching method. Add a new endpoint and you add a method plus one branch - not a whole new class and mapping. The cross-cutting work (logging, auth) can happen once, at the top of `doGet`, before any branch.

💡 Look at what that `if`/`else` ladder really is: **a routing table you wrote by hand.** "If the path is X, call handler Y." You've moved the routing decision out of the container's table and into your own code, where you control it. That single shift is the foundation of essentially every Java web framework.

## This *is* DispatcherServlet

💡 Now the reveal. Over in [Spring MVC](/guides/spring-framework-from-zero), there's one object that catches every request: the **`DispatcherServlet`**. You may have read that phase and taken it on faith. Here's what it actually is - *the exact thing you just built.*

`DispatcherServlet` is a servlet mapped to `/` (the catch-all). Every request in a Spring web app flows through it. It then consults a routing table - Spring calls it the **HandlerMapping** - to find which `@Controller` method should handle this path and HTTP method, and dispatches to it. That's your `if`/`else` ladder, except industrial-strength.

And the annotations you sprinkle on controllers?

```java
@GetMapping("/products")
public List<Product> showProducts() { ... }
```

*What just happened:* `@GetMapping("/products")` is nothing more than an **entry in the routing table**. It's the declarative equivalent of your `if ("/products".equals(path))` branch - you're telling the HandlerMapping "register this method under the path `/products` for GET." Spring reads all those annotations at startup and builds the table for you, so you never write the `if`/`else` by hand. But the shape is identical: one front-controller servlet, a routing table, dispatch to a handler.

You just built a tiny `DispatcherServlet` in twenty lines. The real one adds parameter binding, JSON conversion, view resolution, and error handling - but the spine is this pattern, and now you can see it.

## Forwarding within the server

One related tool you'll bump into: a front controller often needs to hand a request *off* to another servlet or a JSP page to render the actual HTML. That's `RequestDispatcher.forward()`:

```java
RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/products.jsp");
dispatcher.forward(req, resp);
```

*What just happened:* The front controller passes the *same* request and response on to `products.jsp`, which renders the page. The browser never knows - it's all one server-side handoff, one HTTP round-trip. This is exactly how Spring MVC reaches a view template after your controller returns a view name.

⚠️ Don't confuse `forward()` with `resp.sendRedirect("/login")`. A forward is *internal* - the server quietly delegates and the URL in the browser stays the same. A redirect sends a `302` back to the browser telling it to make a *brand-new* request to a different URL (two round-trips, the address bar changes). Forward to render a view; redirect to send the user somewhere else (like after a successful form post).

## Why the frameworks won here

💡 Step back and weigh the hand-written front controller plainly. The pattern is great - one entry point, centralized cross-cutting logic - but maintaining that routing table *by hand* is tedious and error-prone. Every endpoint is a new `if` branch. Typo a path string and you get a silent 404. Want to also match on HTTP method, extract a path variable like `/products/{id}`, or pick the right handler by content type? Your `if`/`else` ladder turns into a swamp.

That swamp is precisely the convenience frameworks sell. The front-controller servlet plus a **declarative** routing table - `@GetMapping("/products")` instead of a fragile string comparison - is the value Spring MVC adds *over this exact servlet pattern*. It didn't invent a new way to handle the web; it automated the routing table you'd otherwise hand-write.

You now see the pattern under the magic. And there's one more piece of "framework magic" rooted right here: the logic you'd run *before* every branch - logging, authentication, compression - has a cleaner home than the top of `doGet`. That home is the **filter chain**, and it's where Phase 5 picks up.

## Recap

1. **The container keeps a routing table** mapping URL patterns to servlets. You write entries with `@WebServlet("/path")` (modern) or `<servlet-mapping>` in `web.xml` (classic) - both feed the same table.
2. **URL patterns** come in flavors: exact (`/products`), path prefix (`/products/*`), extension (`*.do`), and the catch-all `/*` that matches every request.
3. **One servlet per URL doesn't scale** - fifty endpoints means fifty classes, fifty mappings, and cross-cutting logic repeated everywhere.
4. **The front-controller pattern** maps *one* servlet to `/*`, inspects the request path, and dispatches to the right handler internally. That dispatch logic is a routing table you write yourself.
5. **`DispatcherServlet` is exactly this pattern** - one servlet on `/`, with a HandlerMapping routing table. `@GetMapping` is just a declarative entry in that table. The convenience frameworks add is automating the table you'd otherwise hand-write.

## Quick check

Make sure the front-controller picture clicked before moving to filters:

```quiz
[
  {
    "q": "What URL pattern does a front-controller servlet use, and why?",
    "choices": [
      "/* - so it catches every request and can route them all internally",
      "/products - so it only handles the products endpoint",
      "*.do - so it only handles legacy file extensions",
      "An exact match per endpoint, one servlet for each"
    ],
    "answer": 0,
    "explain": "The front-controller pattern maps a single servlet to the catch-all /* (or / for DispatcherServlet) so every request flows through one place, which then inspects the path and dispatches to the right handler."
  },
  {
    "q": "In Spring MVC, what is @GetMapping(\"/products\") in front-controller terms?",
    "choices": [
      "An entry in the routing table the DispatcherServlet consults - the declarative version of an if-branch on the path",
      "A separate servlet that the container maps directly to /products",
      "A database query that loads the products",
      "A filter that runs before the request reaches any servlet"
    ],
    "answer": 0,
    "explain": "@GetMapping registers the method under a path in Spring's HandlerMapping (its routing table). It's the declarative equivalent of the hand-written if (\"/products\".equals(path)) branch in your own front controller."
  },
  {
    "q": "What is the difference between RequestDispatcher.forward() and response.sendRedirect()?",
    "choices": [
      "forward() is an internal server-side handoff (same request, URL unchanged); sendRedirect() tells the browser to make a new request to a different URL",
      "forward() changes the browser URL; sendRedirect() keeps it the same",
      "They are identical, just different names",
      "forward() is for GET requests and sendRedirect() is for POST requests"
    ],
    "answer": 0,
    "explain": "A forward delegates internally within the server - same request/response, one round-trip, URL stays put (used to render a view). A redirect sends a 302 so the browser issues a brand-new request to another URL - two round-trips, the address bar changes."
  }
]
```


---

# Filters & the Chain

Here's a problem you hit on every real web app within about a week. You want to log every request. Then you want to time every request. Then someone says "we need to check the auth token before *any* endpoint runs." Where does that code go?

If your answer is "in every servlet," stop - that's how you end up copy-pasting the same six lines into forty handlers and forgetting one. The Servlet API has a purpose-built place for exactly this kind of code, and once you see it, you'll recognize it in every web framework you ever touch. It's called a **filter**, and the way filters stack up is the literal origin of the word "middleware."

## What a filter actually is

📝 **A filter is code that intercepts a request *before* it reaches your servlet - and gets to touch the response on the way back out - without the servlet knowing it exists.**

Picture the request as a visitor walking toward a room (your servlet). A filter is a doorway the visitor must pass through to get in. The doorway can inspect the visitor, log their arrival, check their ID, reject them outright, or wave them through. When the visitor leaves the room, they walk back out through the same doorway, which gets one more chance to act - on the response this time.

The servlet on the other side has no idea any of this happened. It just receives a request and produces a response, exactly as in [Phase 1](01-what-a-servlet-is.md). That separation is the whole point: your business logic stays clean, and the *cross-cutting* concerns - the ones that apply to many or all requests - live somewhere else.

📝 What kinds of things go in filters? The classic list:

- **Logging** - record every request's method, path, and timing.
- **Authentication / authorization** - check a token or session before any handler runs.
- **Compression** - gzip the response on the way out.
- **CORS** - add the cross-origin headers browsers demand.
- **Timing / metrics** - measure how long requests take.

💡 Hold onto this, because it's the reveal at the end of the phase: **this is what every framework means by "middleware."** Express calls it middleware. ASP.NET calls it middleware. Django calls it middleware. Spring calls it the filter chain. They are all the same idea, and that idea is *the servlet filter* - code that sits in the request's path before your handler.

## `doFilter` and the one line everyone forgets

A filter is a class implementing `jakarta.servlet.Filter`, which has a single method that matters: `doFilter`.

📝 The mechanism is a sandwich. Inside `doFilter` you write your **before** logic, then you call `chain.doFilter(req, res)` to pass control onward - to the next filter, or eventually to the servlet - and when *that* returns, control comes back to you and you run your **after** logic. The request goes in through the top of the sandwich; the response comes back out through the bottom.

```java
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;

@WebFilter("/*")   // apply to every path
public class TimingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        // --- BEFORE: runs on the way in ---
        long start = System.currentTimeMillis();
        String path = ((HttpServletRequest) req).getRequestURI();

        chain.doFilter(req, res);   // hand off to the next filter / the servlet

        // --- AFTER: runs on the way back out ---
        long elapsed = System.currentTimeMillis() - start;
        System.out.println(path + " took " + elapsed + "ms");
    }
}
```

*What just happened:* `@WebFilter("/*")` registers this filter for every request. On the way in we grab a timestamp and the path. The single line `chain.doFilter(req, res)` is the hinge of the whole thing - it passes the request down the line to whatever comes next. Everything *above* that line runs before your servlet; everything *below* it runs after the servlet has produced its response. So `elapsed` is the real wall-clock time the request spent in the application, printed on the way back out.

⚠️ **The number-one filter bug, by a mile: forgetting `chain.doFilter`.** If you leave that line out, the request stops dead inside your filter. It never reaches the next filter, never reaches the servlet, and the client gets an empty or broken response with no error to explain why. When a filter "swallows" requests and you're staring at a blank page, the first thing to check is whether you actually called `chain.doFilter`. Calling it is opting *in* to letting the request continue - silence means "stop here."

## The chain: an onion of filters

You rarely have just one filter. You have several - a logging filter, an auth filter, a CORS filter - and they don't run independently. They run **in order**, each one wrapping the next, forming a **chain**.

📝 The shape to picture is an onion. The request travels *inward* through each filter's "before" half, reaches the servlet at the core, and then the response travels *outward* through each filter's "after" half - in reverse order. The first filter to see the request is the last one to see the response.

```mermaid
flowchart LR
  Req[Request] --> A[Filter A: before]
  A --> B[Filter B: before]
  B --> S[Servlet]
  S --> Bout[Filter B: after]
  Bout --> Aout[Filter A: after]
  Aout --> Resp[Response]
```

*What just happened:* The request enters Filter A, which does its before-work and calls `chain.doFilter` - handing off to Filter B, which does *its* before-work and hands off to the servlet. The servlet produces the response, and now we unwind: control returns to B's after-work, then to A's after-work, and finally out to the client. That reversal is why a filter can wrap the whole downstream operation - A's before runs first and A's after runs last, so A genuinely surrounds everything inside it. A timing filter placed first measures the entire chain plus the servlet.

So how is the order decided? With `@WebFilter` annotations, ordering across multiple filters is **not guaranteed** by the spec - annotation-based filters run in an unspecified order. When order matters (and for auth, it always does), declare your filters in `web.xml` instead, where the order of `<filter-mapping>` elements is the order they run:

```java
// web.xml ordering is explicit and reliable:
//
// <filter-mapping> for CorsFilter      ← runs 1st
// <filter-mapping> for AuthFilter      ← runs 2nd
// <filter-mapping> for LoggingFilter   ← runs 3rd
```

*What just happened:* In `web.xml`, filters run in the order their `<filter-mapping>` entries appear. That gives you the deterministic control you need - for example, putting CORS before auth so preflight requests aren't rejected, and auth before everything else so unauthenticated requests die early. ⚠️ If you mix `@WebFilter` and `web.xml`, you lose predictable ordering; pick one approach for any set of filters whose order matters.

## A real filter: gatekeeping with auth

Logging and timing are gentle - they always call `chain.doFilter` and let the request through. The interesting case is a filter that sometimes says **no**. This is the shape of authentication middleware everywhere, so it's worth seeing in full.

```java
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter("/api/*")   // guard everything under /api
public class AuthFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        String token = request.getHeader("Authorization");

        if (token == null || !token.equals("Bearer letmein")) {
            // REJECT: set 401 and DO NOT call chain.doFilter
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);   // 401
            response.getWriter().write("Unauthorized");
            return;   // the request dies here - the servlet never runs
        }

        // PASS: token is valid, let the request continue
        chain.doFilter(request, response);
    }
}
```

*What just happened:* The filter reads the `Authorization` header. If it's missing or wrong, the filter writes a `401 Unauthorized` response and `return`s **without** calling `chain.doFilter` - so the request is turned away right here and the protected servlet never executes. If the token checks out, `chain.doFilter` lets the request flow on as normal. Notice that here, *not* calling the chain is the deliberate, correct behavior - the opposite of the timing-filter bug. The decision to call or skip `chain.doFilter` is the entire power of a filter: it's the gate.

💡 Look at the structure: a single place that inspects every incoming request and decides "in or out" before any handler runs. That is *exactly* the shape of authentication in every framework. You've just written the bones of what big libraries dress up with tokens, sessions, and role checks.

## The reveal: this IS framework "middleware"

Now zoom out, because you've just learned something bigger than the Servlet API.

💡 Remember [Spring Security from the Spring Boot guide](/guides/spring-boot-from-zero)? Its core mental model is "a chain of filters every request passes through before reaching your controller - each one asking a single question and letting the request continue or stopping it cold." Read that sentence again. It's the chapter you just finished. **Spring Security is, quite literally, a chain of servlet filters.** The `SecurityFilterChain` you configure there is configuring the exact mechanism in this phase. Spring's authentication filter is your `AuthFilter` with years of hardening; its "permit or reject" decisions are `chain.doFilter` called or skipped.

And it doesn't stop at Java. As [What a Framework Even Is](/guides/what-a-framework-even-is) lays out, "middleware" is a universal pattern - Express's `app.use((req, res, next) => ...)` is a filter, and `next()` is `chain.doFilter`; ASP.NET's middleware pipeline with `await next()` is the same onion; Django's middleware classes wrap the view identically. Different languages, identical idea: **the request passes through layers before your code runs, and each layer can observe, modify, or stop it.**

That phrase - "the request passes through layers before your code" - is something you may have read in a dozen framework docs and accepted as magic. It isn't magic. It's the filter chain, and you now know the root mechanism that every one of those frameworks is dressing up. When you meet the next framework's middleware, you won't be learning a new concept; you'll be learning new syntax for this one.

Next up, [Phase 6](06-sessions-and-state.md): we tackle how the server *remembers* a user between requests - sessions and state - which is the other half of how auth actually works in practice.

## Recap

1. 📝 A **filter** intercepts a request before it reaches your servlet, and touches the response on the way back, without the servlet knowing. It's where cross-cutting concerns live: logging, auth, compression, CORS, timing.
2. 📝 `doFilter` is a sandwich: before-logic, then `chain.doFilter(req, res)` to pass control onward, then after-logic on the way back. ⚠️ Forgetting `chain.doFilter` kills the request silently - it never reaches the servlet.
3. 📝 Filters form a **chain** - an onion. The request goes in through each filter's before-half to the servlet, then the response comes back out in reverse order. First in, last out.
4. Ordering with `@WebFilter` is unspecified; use `web.xml` `<filter-mapping>` order when sequence matters (e.g. CORS before auth, auth before everything).
5. An **auth filter** checks a token and either rejects (set 401, skip `chain.doFilter`) or passes through. Skipping the chain is the gate - that's the shape of authentication everywhere.
6. 💡 This is the root of framework **middleware**: Spring Security is a chain of servlet filters; Express/ASP.NET/Django "middleware" is the same pattern. You now know the mechanism behind "the request passes through layers before your code."

## Quick check

Make sure the core idea - and the one line everyone forgets - actually stuck:

```quiz
[
  {
    "q": "What does calling chain.doFilter(req, res) inside a filter do?",
    "choices": [
      "Passes control to the next filter in the chain, or to the servlet if it's the last filter",
      "Immediately returns the response to the client and ends the request",
      "Restarts the filter from the beginning",
      "Logs the request to the servlet container's console"
    ],
    "answer": 0,
    "explain": "chain.doFilter hands the request onward - to the next filter, or to the servlet if this is the last filter. Code before it runs on the way in; code after it runs on the way back out. Omitting it stops the request dead inside your filter."
  },
  {
    "q": "An authentication filter decides a request is NOT authorized. What should it do?",
    "choices": [
      "Set the response status (e.g. 401) and return WITHOUT calling chain.doFilter, so the servlet never runs",
      "Call chain.doFilter anyway and let the servlet decide",
      "Throw an exception so the container picks a status code",
      "Remove the Authorization header and continue the chain"
    ],
    "answer": 0,
    "explain": "To reject, the filter writes the response (like 401 Unauthorized) and returns without calling chain.doFilter. That stops the request right there - the protected servlet never executes. Skipping the chain is exactly how a filter acts as a gate."
  },
  {
    "q": "Why is Spring Security described as 'a chain of servlet filters'?",
    "choices": [
      "Because it literally is one - its SecurityFilterChain is built on the servlet filter mechanism, the same pattern every framework calls 'middleware'",
      "Because it replaces the servlet container entirely with its own request handler",
      "Because it only works with Spring's @RestController and not plain servlets",
      "Because 'filter chain' is just a marketing name with no technical meaning"
    ],
    "answer": 0,
    "explain": "Spring Security is built directly on servlet filters: its filter chain installs ordered filters that each ask one question and call (or skip) the chain. Express, ASP.NET, and Django 'middleware' are the same idea in other languages - the filter chain is the root mechanism."
  }
]
```


---

# Sessions & State

You log into a site, click around five pages, and it still knows who you are. Feels obvious. It is not
obvious at all - because the protocol underneath has no memory whatsoever. Every framework's "current
user," every shopping cart, every "stay logged in" rests on a single trick the Servlet API gives you raw.
This phase is that trick, demystified.

## The problem: HTTP forgets you between requests

Start with the uncomfortable fact, because everything else is a response to it.

📝 **HTTP is stateless.** Each request is a self-contained event. The server reads it, answers it, and
keeps nothing about you for next time. Two requests from the same browser, ten seconds apart, arrive
looking like two strangers - the server has no built-in way to know they're the same person. That's not a
bug; it's the design that lets HTTP scale (see [/guides/http-explained](/guides/http-explained) and the
statelessness discussion in [/guides/http-and-json-api-basics](/guides/http-and-json-api-basics)). A
server holding nothing per-connection can field requests from millions of browsers without drowning.

But real apps need memory. "This user is logged in." "Their cart has three items." "They're on step 2 of
checkout." None of that survives in a stateless world on its own. So how does a site remember you across
requests when the protocol throws you away after each one?

The answer isn't to make HTTP stateful. It's to carry a small token of identity on *every* request, and
keep the real memory on the server, keyed by that token. That token is a **session id**, and the carrier
is a **cookie**.

## Cookies + a session id: the mechanism

Here's the whole dance in one sentence: **the server gives the browser an id once, the browser hands that
id back on every later request, and the server uses it to find your stored data.**

Walk through it as an HTTP exchange. First request - the browser has nothing to identify itself with, so
the server creates a fresh session and tells the browser to remember its id:

```http
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: JSESSIONID=3F2A9C1B7E5D4088; Path=/; HttpOnly

<!-- the page -->
```

*What just happened:* the server made a new session, generated a hard-to-guess id (`3F2A9C1B7E5D4088`),
and sent it back in a `Set-Cookie` header. `JSESSIONID` is the conventional name the servlet container
uses. The browser quietly stores this cookie. Nothing about *you* travels in that header - just an opaque
key.

On every following request to the same site, the browser automatically attaches the cookie:

```http
GET /cart HTTP/1.1
Host: example.com
Cookie: JSESSIONID=3F2A9C1B7E5D4088
```

*What just happened:* the browser sent the `Cookie` header without being asked - that's what browsers do
with stored cookies. The server reads `JSESSIONID=3F2A9C1B7E5D4088`, looks up the session with that id in
its own memory, and instantly knows: this is the same visitor from before, here's their cart. The protocol
is still stateless on the wire; the *id riding along on each request* is what stitches the requests into a
session.

📝 **The cookie is just the key; the data lives on the server.** This is the part to hold onto. The cookie
carries a meaningless-looking id, nothing more. The actual contents - who you are, what's in your cart - 
sit in a map on the server side, looked up by that id. Lose the cookie and you lose the *key*, not the
data (the data's still there; you just can't point at it anymore).

```mermaid
sequenceDiagram
  participant B as Browser
  participant S as Server
  B->>S: First request (no cookie)
  S->>S: Create session + id
  S-->>B: Set-Cookie: JSESSIONID=...
  B->>S: Later request (Cookie: JSESSIONID=...)
  S->>S: Look up session by id
  S-->>B: Response using your data
```

## `HttpSession` in code

The Servlet API wraps that whole mechanism in one object so you never touch the cookie or the id by hand.
You ask for the session; the container handles `Set-Cookie`, the lookup, and the id generation for you.

Storing something during, say, a login:

```java
protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws IOException {
    User user = authenticate(request);          // your own check

    HttpSession session = request.getSession(); // creates one if none exists
    session.setAttribute("user", user);         // stash it server-side

    response.sendRedirect("/dashboard");
}
```

*What just happened:* `request.getSession()` returns the existing session for this browser, or creates a
fresh one if there's no valid `JSESSIONID` cookie yet - and when it creates one, the container adds the
`Set-Cookie` header to the response for you. `setAttribute("user", user)` puts the user object into
server-side storage under the key `"user"`. We never generated an id or wrote a cookie ourselves; the API
did it.

Reading it back on a *later* request - a different HTTP request entirely, stitched to this one only by the
cookie:

```java
protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException {
    HttpSession session = request.getSession(false); // don't create; just look
    User user = (session == null) ? null : (User) session.getAttribute("user");

    if (user == null) {
        response.sendRedirect("/login");
        return;
    }
    response.getWriter().write("Welcome back, " + user.name());
}
```

*What just happened:* `getSession(false)` asks "is there already a session?" without creating one - the
`false` matters, since a bare `getSession()` would manufacture an empty session and a cookie even for a
logged-out visitor. We pull `"user"` back out with `getAttribute`. The object is the *same one we stored
earlier*, because it lived on the server the whole time; the request just arrived carrying the id that
finds it. On logout you tear it all down:

```java
HttpSession session = request.getSession(false);
if (session != null) {
    session.invalidate(); // drop the server-side data + the id
}
```

*What just happened:* `invalidate()` discards the session and its attributes on the server, so the old
`JSESSIONID` now points at nothing. The next request starts clean.

## Sessions vs the stateless/token alternative

Server-side sessions are simple and they work - but they have a cost, and the cost shows up the moment you
run more than one server.

⚠️ **Sessions hold memory per user, and that complicates scaling.** Every active session is RAM on a
specific server. Run two servers behind a load balancer and request #2 might land on the box that *doesn't*
have your session - your id finds nothing, and you look logged out. The usual fixes are **sticky sessions**
(the balancer pins each user to the server that holds their session) or a **shared session store** (sessions
live in Redis or a database that all servers read). Both work; both add moving parts and a thing that can
fall over.

📝 **The modern alternative for APIs: stateless tokens.** Instead of the server remembering you, the
*client* carries the state. On login the server hands back a signed token - a **JWT** is the common form - 
that itself contains the claims ("user 42, role admin, expires at noon"), cryptographically signed so it
can't be forged. The client sends it on each request (typically `Authorization: Bearer <token>`), and the
server just verifies the signature. No session map, no per-user RAM, nothing to share between servers - 
any server can validate any request. (This ties straight into the auth/security guides, where token
verification and signing live in detail.)

So which fits when?

- **Server-side sessions** - great for classic server-rendered web apps where the browser handles cookies
  for free, and you want the ability to revoke a session instantly (just delete it server-side).
- **Stateless tokens** - great for APIs, mobile clients, and multi-server fleets where you don't want
  shared session state. The trade-off: a token is valid until it expires, so instant revocation takes extra
  machinery (a denylist, short lifetimes + refresh tokens).

Neither is "correct." They're different answers to the same stateless-HTTP problem: sessions keep the
memory on the server, tokens push it to the client.

## What frameworks add

You now have the raw mechanism. Everything a framework gives you here is built directly on `HttpSession`.

💡 **Frameworks decorate this layer, they don't replace it.** Spring's `@SessionScope` beans? A bean whose
lifetime is tied to an `HttpSession` - the container's session underneath, with dependency-injection
ergonomics on top. **Spring Session** swaps the server-side store for Redis (the shared-store fix above)
without changing your code - your `getAttribute`/`setAttribute` mental model is intact; only *where* the
session lives moves. **Spring Security**'s entire "the user is logged in" machinery sits on the session you
just saw: it puts an authentication object into the session and reads it back on each request, exactly like
our `user` example, with a lot more rigor.

That rigor matters, because raw sessions have sharp edges frameworks help you handle - and you should know
them even when a framework is doing the work:

⚠️ **Session security basics.**
- **Regenerate the id on login** - otherwise an attacker who plants a known `JSESSIONID` before you log in
  can ride your authenticated session afterward. This is **session fixation**; the fix is a new id at the
  moment privilege changes.
- **`HttpOnly` and `Secure` cookies** - `HttpOnly` keeps JavaScript from reading the session cookie (so an
  XSS bug can't steal it); `Secure` keeps it off plain HTTP, so it only travels over HTTPS.
- **Timeouts** - sessions should expire after inactivity, so a forgotten-open session on a shared computer
  doesn't stay valid forever.

You can now see the real thing beneath "the user is logged in": an id in a cookie, data in a server-side
map, a few security rules around the id. That's the last piece of the bare mechanism - next we step back up
and watch the whole Servlet API reappear, named differently, inside the frameworks themselves.

## Recap

1. **HTTP is stateless** - each request is independent and the server keeps nothing about you between
   them. Sessions are how stateful behavior (login, carts) gets built on top of a forgetful protocol.
2. The mechanism: the server creates a session and sends a **session id** in a `Set-Cookie` header; the
   browser returns it in the `Cookie` header on every later request; the server uses it to look up your
   data. The container's conventional cookie name is `JSESSIONID`.
3. **The cookie is just the key - the data lives server-side.** The cookie carries an opaque id; the real
   contents sit in a map on the server, keyed by that id.
4. In code: `request.getSession()` (or `getSession(false)` to avoid creating one), `setAttribute` /
   `getAttribute` to store and read across requests, `invalidate()` on logout. The API handles the cookie
   and id for you.
5. ⚠️ Sessions cost per-user memory and complicate multi-server scaling (sticky sessions or a shared store
   like Redis). The stateless alternative for APIs is a signed **token (JWT)** the client carries - no
   server session at all.
6. 💡 Frameworks (`@SessionScope`, Spring Session, Spring Security) are built on `HttpSession`. Mind the
   security basics: regenerate the id on login (fixation), `HttpOnly`/`Secure` cookies, and timeouts.

## Quick check

Three questions on the ideas that have to stick:

```quiz
[
  {
    "q": "When a browser keeps you 'logged in' across requests, where does your actual session data live?",
    "choices": [
      "Server-side, in storage keyed by a session id; the cookie only carries that id",
      "Entirely inside the cookie, which is why it must be encrypted",
      "In the HTTP protocol itself, which tracks connections per user",
      "Nowhere - the server re-authenticates you from scratch on every request"
    ],
    "answer": 0,
    "explain": "The cookie carries an opaque session id (the key). The real data sits in a server-side map looked up by that id. Lose the cookie and you lose the key, not the data."
  },
  {
    "q": "What does request.getSession(false) do that request.getSession() does not?",
    "choices": [
      "It returns the existing session or null, without creating a new one (and no new cookie)",
      "It permanently disables sessions for this request",
      "It creates a session but skips sending the JSESSIONID cookie",
      "It reads the session without locking it for concurrent requests"
    ],
    "answer": 0,
    "explain": "getSession(false) looks up an existing session and returns null if there isn't one. Plain getSession() would manufacture an empty session - and a Set-Cookie header - even for a logged-out visitor."
  },
  {
    "q": "Why do stateless tokens (like a JWT) scale across multiple servers more easily than server-side sessions?",
    "choices": [
      "The client carries the state in the signed token, so any server can verify a request without shared session memory",
      "Tokens are stored in the load balancer, which all servers read from",
      "Tokens never expire, so servers never need to look anything up",
      "Each server keeps its own copy of every user's session in RAM"
    ],
    "answer": 0,
    "explain": "A signed token carries its claims with it, so every server just verifies the signature - no per-user RAM and nothing to share. Server-side sessions need sticky routing or a shared store (e.g. Redis) to work across a fleet."
  }
]
```


---

# From Servlets to Frameworks

Go back to the start of this guide for a second. A framework was a black box: you annotated a method, requests showed up, responses went out, and somewhere in the middle a lot of magic happened that you couldn't name.

Look at what you can see now. You know a request arrives at a **container**, which hands it to a **servlet** as an `HttpServletRequest` and an `HttpServletResponse`. You know that servlet is *one instance serving many threads*, which is exactly why shared mutable state bites you. You know a single **front-controller** servlet can route every URL to the right handler. You know **filters** wrap your servlet to run code before and after. You know **sessions** stitch state across a stateless protocol. That's not trivia - that's the whole shape of Java web, and you've now seen all of it bare.

This last phase is the payoff. We're not adding new mechanism. We're pointing the X-ray vision you just built at the frameworks you'll actually use at work, and watching the magic turn into machinery you can already name.

## Mapping the magic to the mechanism

💡 Here's the thing worth reading slowly: every "feature" in a Java web framework is a convenience over something in this guide. Once you've seen the bare version, the framework version stops being a spell and becomes a name for a thing you understand.

```mermaid
flowchart LR
  DS["DispatcherServlet"] --> FC["Front controller (Phase 4)"]
  GM["@GetMapping"] --> RT["Routing table entry (Phase 4)"]
  SEC["Spring Security"] --> FL["Filter chain (Phase 5)"]
  MW["'Middleware'"] --> FL
  SS["@SessionScope"] --> HS["HttpSession (Phase 6)"]
  TS["Thread-safe controllers"] --> ON["One instance, many threads (Phase 2)"]
  JX["JAX-RS"] --> SV["Dispatched by a servlet"]
```

Reading that left to right, in plain words:

- **Spring MVC's `DispatcherServlet`** is a front-controller servlet - the exact pattern from [Phase 4](04-mapping-and-the-front-controller.md). The name even says it out loud: it's a *servlet* that *dispatches*. One servlet catches every request and routes it onward.
- **`@GetMapping("/users")`** is an entry in that front controller's routing table. In Phase 4 you wrote the routing map by hand, matching paths to handlers. The annotation is the framework filling in that same table for you.
- **Spring Security** is a servlet **filter chain** - [Phase 5](05-filters-and-the-chain.md), generalized. Authentication, authorization, CSRF protection: each is a filter that runs before your code, can short-circuit the request, or pass it along the chain.
- **"Middleware,"** anywhere you've heard the word, is a filter underneath. Different ecosystems coined a friendlier name for "code that wraps the request before and after your handler." You built one in Phase 5.
- **`@SessionScope`** (a bean that lives for the length of a user's session) is `HttpSession` from [Phase 6](06-sessions-and-state.md), dressed up. The framework stashes the object in the session and pulls it back out per user.
- **Thread-safe controllers** are the one-instance-many-threads rule from [Phase 2](02-the-servlet-container-and-lifecycle.md). Your Spring `@Controller` is a singleton, shared across every concurrent request - which is why you keep request state in locals and parameters, not in fields.
- **JAX-RS** (the Jakarta EE REST standard) is dispatched by a servlet too. A single servlet at the front receives requests and routes them to your annotated resource methods. Same shape, different vendor.

That covers two whole worlds. Spring's web layer lives on top of this - see [Spring Framework From Zero](/guides/spring-framework-from-zero). So does Jakarta's - see [Jakarta EE From Zero](/guides/jakarta-ee-from-zero). Underneath both: a servlet, a front controller, and a filter chain.

## Why you still reach for a framework

Let me be direct, because a roots guide that pretends raw servlets are enough would be doing you a disservice: you don't want to build a real app out of bare servlets. It's genuinely tedious.

You'd hand-write the routing table and keep it in sync by hand. You'd serialize and deserialize JSON yourself, field by field. There's no dependency injection, so you'd wire every collaborator manually. Validation, content negotiation, error pages, exception mapping - all boilerplate you'd write and maintain. The frameworks exist because smart people got tired of writing that code a thousand times, and the conveniences they added are real and worth having.

💡 So here's the point of having learned this: you almost certainly won't write servlets at work - you'll write Spring or Jakarta. What changed is that you now understand *what those frameworks are conveniences over*. That's the entire purpose of a roots guide. When `DispatcherServlet` shows up in a stack trace, you don't flinch - you know it's a front-controller servlet and you can reason about it. When a security filter blocks a request, you know it's a filter in a chain and you know where to look. The framework didn't get simpler; you got the map.

## A note on the modern shift

One plain caveat, because the picture isn't frozen. A newer wave of stacks - reactive, non-blocking, built on event-loop servers like Netty (think Spring WebFlux, or parts of Quarkus) - steps *outside* the classic servlet model. They trade the familiar thread-per-request shape for a smaller pool of threads juggling many requests, and in doing so they bypass the servlet container you met here.

Don't let that unsettle what you just learned. The servlet API still underlies the **vast majority** of Java web running in production today, and the mental model - a request arriving, something routing it, filters wrapping it, a response going back - carries over even to the reactive world; the plumbing differs, the shape rhymes. Knowing the servlet model is still the right foundation, and it's the one almost everything you'll touch is built on.

## What to build - and a last word

📝 Reading got you here. One small build will lock it in for good. Here's the exercise that cements everything:

Build a tiny app with **raw servlets** - no framework. Give it a single front-controller servlet that routes a couple of URLs (Phase 4). Add one **filter** that checks for a logged-in user and redirects to a login page if there isn't one (Phase 5). Use an **`HttpSession`** to remember who's logged in across requests (Phase 6). Keep it small - a couple of pages and a login.

Then do the magic trick: imagine rebuilding the same thing in Spring or Jakarta, and watch it collapse. The routing servlet becomes `@GetMapping` methods. The auth filter becomes a few lines of Spring Security config. The session juggling becomes `@SessionScope` or just disappears. *That contrast* - the bare version next to the convenient version - is what fuses the two layers in your head permanently. You'll never look at a framework annotation the same way again.

When you want the authoritative reference, go to the **Jakarta Servlet specification and its API docs**. They're precise, they're the source of truth, and now that you have the concepts, they'll read as confirmation rather than fog.

The line to carry out of this whole guide: **every Java web framework is conveniences over a servlet, a front controller, and a filter chain - and now you can see all three.** The magic was always just this. Go build the small thing, and watch it stay gone.

## Recap

1. **The X-ray vision is the whole point.** You can now see, under any Java web framework, the request lifecycle, the front controller, the filter chain, and sessions - the bare mechanism this guide built.
2. **Each framework "feature" maps to something you know:** `DispatcherServlet` is a front controller (Phase 4), `@GetMapping` is a routing-table entry, Spring Security and "middleware" are filter chains (Phase 5), `@SessionScope` is `HttpSession` (Phase 6), thread-safe controllers are the one-instance-many-threads rule (Phase 2), and JAX-RS is dispatched by a servlet too.
3. **Frameworks earn their keep.** Raw servlets are tedious - manual routing, manual serialization, no DI, endless boilerplate. Frameworks add the conveniences; this guide showed you *what they're conveniences over*.
4. **A modern caveat:** reactive/Netty-based stacks (WebFlux, parts of Quarkus) step outside the classic servlet model, but the servlet API still underlies the vast majority of Java web, and the mental model carries over.
5. **Build to cement it:** a tiny raw-servlet app with a front controller, an auth filter, and a session - then notice how a framework would collapse it. The Jakarta Servlet spec is your authoritative source.

## Quick check

One last check - the mappings that turn frameworks from magic into mechanism:

```quiz
[
  {
    "q": "Spring MVC's DispatcherServlet is, mechanically, an example of what?",
    "choices": [
      "A front-controller servlet that routes every request onward",
      "A brand-new protocol that replaces HTTP",
      "A database connection pool",
      "A reactive event loop unrelated to servlets"
    ],
    "answer": 0,
    "explain": "The name says it: it's a servlet that dispatches. One front-controller servlet catches every request and routes it to the right handler - exactly the front-controller pattern from Phase 4. @GetMapping is just an entry in its routing table."
  },
  {
    "q": "When a framework talks about 'middleware' or a security layer like Spring Security, what servlet concept is it built on?",
    "choices": [
      "The servlet filter chain",
      "The HttpSession",
      "The servlet's destroy() callback",
      "A second servlet container running alongside the first"
    ],
    "answer": 0,
    "explain": "'Middleware' anywhere, and Spring Security specifically, is a servlet filter chain underneath - code that wraps your servlet to run before and after the request, able to pass it along or short-circuit it. That's Phase 5, generalized."
  },
  {
    "q": "Which statement about the modern reactive shift is accurate?",
    "choices": [
      "Some reactive stacks (WebFlux, parts of Quarkus) bypass the classic servlet model, but the servlet API still underlies the vast majority of Java web",
      "Reactive stacks have completely replaced servlets everywhere",
      "The servlet API was never used in production",
      "Reactive frameworks have nothing to do with HTTP requests and responses"
    ],
    "answer": 0,
    "explain": "Reactive, non-blocking stacks built on event-loop servers like Netty step outside the thread-per-request servlet model - but they're the minority. The servlet API still underlies most Java web in production, and the request/route/filter/response mental model carries over."
  }
]
```
