What is a Servlet — and Why Does a Spring Developer Need to Know?
A Servlet is a Java class that receives an HTTP request
and produces an HTTP response. That's the entire contract —
HttpServletRequest in, HttpServletResponse out.
The Servlet specification (now jakarta.servlet.*) defines this
contract; Tomcat, Jetty, and Undertow are implementations that run servlets
inside a Servlet Container.
The reason a Spring developer needs to understand this: Spring MVC's
DispatcherServlet is itself a Servlet — it implements
HttpServlet and is registered in a Servlet Container exactly
like any other servlet. Every HTTP request your Spring Boot application
handles goes through this chain:
/*
* Browser
* │ HTTP request
* ▼
* Tomcat (Servlet Container)
* │ invokes the registered Servlet for this URL
* ▼
* DispatcherServlet ← this IS a Servlet (extends HttpServlet)
* │ Spring's own routing logic
* ▼
* @RestController / @Controller method
* │ your code
* ▼
* HTTP response back to browser
*
* Understanding Servlets = understanding what's running underneath Spring.
*/
In new Spring Boot projects, you almost never write a raw
HttpServlet subclass — Spring MVC handles all routing.
The cases where raw Servlets still appear: integrating a third-party
library that registers its own servlet (e.g. H2 Console,
Swagger UI embeds), legacy codebases pre-Spring MVC, and Jakarta EE
projects on WildFly or Payara that don't use Spring at all. The
concepts transfer directly — request/response, lifecycle, filters —
even if you write them through Spring abstractions.
The Servlet Lifecycle
/*
* Container startup
* │
* ▼
* new HelloServlet() ← container calls the no-arg constructor
* │
* ▼
* init(ServletConfig config) ← called ONCE, before any request is handled
* │ override to set up resources (DB connections, etc.)
* ▼
* ┌──────────────────────────────────────────────┐
* │ For each incoming request (concurrent): │
* │ │
* │ service(request, response) │
* │ ├── doGet() ← GET requests │
* │ ├── doPost() ← POST requests │
* │ ├── doPut() ← PUT requests │
* │ ├── doDelete() ← DELETE requests │
* │ └── ... │
* └──────────────────────────────────────────────┘
* │
* ▼
* destroy() ← called ONCE when container shuts down
* override to release resources
*/
The container creates one servlet instance and routes
all concurrent requests to it across multiple threads simultaneously.
Any instance variable on a servlet is shared across all requests — a
classic race condition waiting to happen. The rule: servlets must have
no mutable instance state. Everything request-specific
lives in local variables inside the handler method, or in the
HttpSession. This is the same reason Spring's
@Controller beans are singletons by default and must also
be stateless.
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;
import java.io.*;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
// ✅ Safe: set once in init(), never changed — effectively final
private String greeting;
// ❌ WRONG: mutable instance variable — race condition under concurrent requests
// private int requestCount = 0;
@Override
public void init() {
greeting = getServletConfig().getInitParameter("greeting"); // read-only after init
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String name = request.getParameter("name"); // ✅ Safe: local variable, per-request
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().println(greeting + ", " + name);
}
@Override
public void destroy() {
// release any resources opened in init()
}
}
HttpServletRequest and HttpServletResponse
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// ── Reading the request ───────────────────────────────────────────────
String name = request.getParameter("name"); // query param or form field
String auth = request.getHeader("Authorization"); // request header
String method = request.getMethod(); // "GET", "POST", etc.
String uri = request.getRequestURI(); // "/myapp/users"
String body = request.getReader().lines() // raw body as string
.collect(java.util.stream.Collectors.joining());
// Attributes (server-side data attached to THIS request, not from client)
request.setAttribute("user", currentUser);
Object user = request.getAttribute("user");
// ── Building the response ─────────────────────────────────────────────
response.setStatus(HttpServletResponse.SC_CREATED); // 201
response.setContentType("application/json;charset=UTF-8");
response.setHeader("Location", "/api/users/123");
response.getWriter().println("{\"id\":123,\"name\":\"" + name + "\"}");
// Forward vs Redirect — different things, different use cases
request.getRequestDispatcher("/WEB-INF/views/success.jsp")
.forward(request, response); // server-side: same request, same URL in browser
response.sendRedirect("/dashboard"); // client-side: new GET request, URL changes
}
forward() transfers control to another resource
(another servlet or JSP) inside the server — the browser's
URL bar never changes, and the original request object is passed along.
sendRedirect() sends a 302 response to the browser, which
then makes a brand-new GET request to the given URL. The classic
pattern for form submissions is POST → process → redirect → GET (the
"Post/Redirect/Get" pattern), specifically to prevent the browser from
resubmitting the form on refresh. Spring MVC's
"redirect:/path" return value does exactly this.
Filters — Cross-Cutting Concerns Before the Servlet
A Filter intercepts requests before they reach any
servlet — and responses on the way back out. The filter chain runs in
declared order; each filter calls chain.doFilter() to pass
control to the next filter or the servlet, or it can short-circuit by
writing directly to the response without calling
chain.doFilter().
@WebFilter("/*") // applies to every URL
public class RequestLoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) req;
long start = System.currentTimeMillis();
chain.doFilter(req, res); // ← hand off to the next filter or the servlet
long elapsed = System.currentTimeMillis() - start;
log.info("{} {} → {}ms", httpReq.getMethod(), httpReq.getRequestURI(), elapsed);
}
}
// Auth filter — short-circuits if token is missing, never calls chain.doFilter()
@WebFilter("/api/*")
public class AuthFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain)
throws IOException, ServletException {
String auth = ((HttpServletRequest) req).getHeader("Authorization");
if (auth == null || !auth.startsWith("Bearer ")) {
((HttpServletResponse) res).sendError(HttpServletResponse.SC_UNAUTHORIZED);
return; // short-circuit — servlet never runs
}
chain.doFilter(req, res);
}
}
Spring Security's entire mechanism — covered in
Spring Security — is a chain
of Filter implementations registered in front of
DispatcherServlet. DelegatingFilterProxy
bridges the Servlet API and the Spring container so that Spring-managed
beans can participate in the filter chain. Understanding raw
Filter here makes the Security filter chain architecture
immediately readable.
JSP — Historical Context, Not a Recommendation
JSP (JavaServer Pages) allowed embedding Java code directly inside HTML, so the server could render dynamic content without routing through a servlet for every page. It was the dominant server-side rendering technology in Java from the late 1990s through the mid-2000s.
<!-- users.jsp — a realistic example showing WHY JSP fell out of favour -->
<%@ page import="java.util.List" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html><body>
<h1>Users</h1>
<%-- Scriptlet (avoid) — mixes Java and HTML, untestable, unmaintainable --%>
<%
List users = (List) request.getAttribute("users");
for (Object u : users) {
out.println("<p>" + u + "</p>"); // XSS risk: no escaping
}
%>
<%-- JSTL (the "clean" JSP way) — still JSP, but at least no scriptlets --%>
<c:forEach var="user" items="${users}">
<p><c:out value="${user.name}"/></p> <%-- c:out escapes HTML --%>
</c:forEach>
</body></html>
JSP's fundamental problem is mixing concerns that don't belong together: presentation logic (HTML structure) and Java code in the same file. This made pages hard to test (no unit testing of JSP), hard to maintain (designers couldn't touch files with Java in them), and prone to XSS when developers forgot to escape output. Three things killed it in practice:
- Thymeleaf (Spring's default today) — pure HTML
templates that work in a browser directly, no Java code in the
template, natural templating syntax with
th:*attributes - Single-Page Applications (React, Vue, Angular) — the server returns JSON, the browser renders HTML entirely client-side; JSP becomes irrelevant
- Freemarker / Mustache — logic-less templating that enforces separation of concerns at the language level
You will encounter JSP in legacy systems. You should understand how it works, recognise the XSS risks of raw scriptlets, and know how to migrate it — not write new JSP in 2025.
The MVC Pattern with Servlets + JSP
/*
* Classic Servlet + JSP MVC — the pattern that Spring MVC formalized:
*
* Browser → GET /users
* │
* ▼
* UserListServlet.doGet() ← Controller: fetch data, put in request scope
* userService.findAll()
* request.setAttribute("users", users)
* request.getRequestDispatcher("/WEB-INF/views/users.jsp").forward(req, res)
* │
* ▼
* users.jsp ← View: render the data, no business logic
* ${users} — display the list
*
* Spring MVC is exactly this pattern, with DispatcherServlet as the single
* front controller instead of one servlet per URL, and Thymeleaf instead of JSP.
*/
@WebServlet("/users")
public class UserListServlet extends HttpServlet {
private UserService userService; // can't @Inject here without CDI — see CDI page
@Override
public void init() {
userService = new UserService(); // manual wiring without a DI container
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
req.setAttribute("users", userService.findAll());
req.getRequestDispatcher("/WEB-INF/views/users.jsp").forward(req, res);
}
}
Session Management
HTTP is stateless — each request carries no memory of previous ones. The
Servlet API's HttpSession is the container-managed mechanism
for maintaining state across requests for the same user, backed by a session
ID stored in a cookie (JSESSIONID by default).
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
if (authenticate(req.getParameter("username"), req.getParameter("password"))) {
// getSession(true) = create if absent; getSession(false) = return null if absent
HttpSession session = req.getSession(true);
session.setAttribute("username", req.getParameter("username"));
session.setMaxInactiveInterval(30 * 60); // 30 minutes; -1 = never expire
res.sendRedirect("/dashboard"); // PRG pattern — prevents form resubmission
} else {
res.sendRedirect("/login?error=invalid");
}
}
// Logout — always invalidate, never just remove attributes
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
HttpSession session = req.getSession(false); // false = don't create if absent
if (session != null) {
session.invalidate(); // destroys the session — all attributes gone, ID invalidated
}
res.sendRedirect("/login");
}
HttpSession is a server-side state store — it breaks
horizontal scaling without sticky sessions or a shared session store
(Redis, Hazelcast). This is precisely why REST APIs prefer stateless
authentication (JWT bearer tokens) over session-based auth. The
Spring Security page covers both approaches and explains exactly when
to use each. Sessions remain appropriate for traditional server-rendered
web applications; they are an anti-pattern in REST APIs.
WebSocket — Bidirectional Communication from the Servlet Container
The WebSocket spec (jakarta.websocket.*) is part of the same
Servlet container infrastructure. A WebSocket endpoint is deployed
alongside servlets in the same WAR and managed by the same container —
which is why it belongs in this section rather than a separate page.
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
@ServerEndpoint("/ws/chat") // URL: ws://host/app/ws/chat
public class ChatEndpoint {
@OnOpen
public void onOpen(Session session) {
log.info("Client connected: {}", session.getId());
}
@OnMessage
public void onMessage(String message, Session session)
throws IOException {
// Echo back to the same client
session.getBasicRemote().sendText("Echo: " + message);
}
@OnError
public void onError(Throwable error, Session session) {
log.error("WebSocket error on session {}", session.getId(), error);
}
@OnClose
public void onClose(Session session, CloseReason reason) {
log.info("Connection closed: {} — {}", session.getId(), reason.getReasonPhrase());
}
}
Spring Boot supports @ServerEndpoint directly — just
declare a ServerEndpointExporter bean to register
Jakarta WebSocket endpoints with the embedded container. Spring also
provides its own higher-level WebSocket + STOMP abstraction
(@MessageMapping) for publish-subscribe messaging patterns.
Both approaches ultimately use the same Servlet container WebSocket
infrastructure covered here.
Interview Questions
Q: What is a Servlet, and what is its relationship with Spring MVC?
A Servlet is a Java class that receives an HTTP request and produces an
HTTP response. Spring MVC's DispatcherServlet is itself a
Servlet — it implements HttpServlet and is registered in a
Servlet Container (Tomcat, Jetty). Every HTTP request in a Spring Boot
application flows through DispatcherServlet before reaching
any @Controller method.
Q: What is the difference between forward() and sendRedirect()?
forward() transfers control to another server-side resource
without the browser knowing — the URL in the address bar doesn't change,
and the original request object is reused. sendRedirect()
sends a 302 to the browser, which makes a brand-new GET request —
the URL changes. The Post/Redirect/Get pattern uses
sendRedirect() after a POST to prevent form resubmission on
browser refresh.
Q: Why shouldn't servlets have mutable instance variables?
The container creates one servlet instance and routes all concurrent
requests to it across multiple threads simultaneously. A mutable instance
variable is shared across all those threads without synchronisation —
a race condition. Request-specific data must live in local variables inside
the handler method, never in fields.
Q: Where does a Servlet Filter run relative to DispatcherServlet, and why does that matter for Spring Security?
A Filter runs at the Servlet Container level, before
DispatcherServlet ever executes. Spring Security registers
its entire security mechanism as a filter chain
(DelegatingFilterProxy → FilterChainProxy)
precisely because it needs to intercept requests before Spring's routing
logic runs — so that unauthenticated requests never reach
DispatcherServlet at all. A Spring
HandlerInterceptor, by contrast, runs inside
DispatcherServlet's pipeline and can't prevent the request
from reaching the dispatcher.
Q: Why is HttpSession an anti-pattern for REST APIs, and what's the correct alternative?
HttpSession is server-side state — it exists on one JVM
instance. Under horizontal scaling, a request hitting a different server
finds no session unless you use sticky sessions (which defeats the purpose
of load balancing) or a shared external session store (Redis, adding
operational complexity). REST's statelessness constraint says each request
must carry all the context the server needs — which is exactly what JWT
bearer tokens provide: the user's identity and claims travel in the
Authorization header on every request, no server-side
state required.
Q: Why did JSP fall out of favour, and what replaced it?
JSP mixed presentation (HTML) with Java code in the same file, making it
untestable, unmaintainable for UI designers, and prone to XSS when
developers forgot to escape output. Three things replaced it: Thymeleaf
(pure HTML templates, no Java in the file), Single-Page Applications
(server returns JSON, browser renders HTML — JSP irrelevant), and
logic-less templating engines (Freemarker, Mustache) that enforce
separation of concerns at the language level.