The Servlet Lifecycle
The Servlet Lifecycle
Every servlet you write lives inside a container — Tomcat, Jetty, WildFly, or any other Jakarta EE-compliant engine. The container owns the object: it decides when to create your servlet, when to destroy it, and how many threads may call it simultaneously. Understanding that contract in depth is what separates developers who debug concurrency problems in five minutes from those who chase them for days.
The Three Lifecycle Methods
The Jakarta Servlet specification defines three methods that mark the stages of a servlet's existence. The container calls them — you override them.
init(ServletConfig config)— called once, right after the container instantiates the servlet. Use it for expensive one-time setup: opening a database connection pool, loading a configuration file, or initialising a cache.service(ServletRequest req, ServletResponse res)— called for every request, potentially by many threads at the same time.HttpServletdispatches each call todoGet,doPost, etc., so you usually override those convenience methods rather thanservicedirectly.destroy()— called once, when the container is shutting down or the servlet is being un-deployed. Release whateverinitacquired: close the pool, flush a write buffer, cancel background threads.
init completes before the first service call. destroy is called only after all in-progress service calls finish. The container enforces this — you get clear boundaries without writing synchronisation code for the phase transitions themselves.
A Fully Annotated Lifecycle Example
The following servlet demonstrates all three methods in context. It opens an imaginary connection pool in init, queries it per request, and closes it in destroy.
Notice the call to super.init(config). HttpServlet.init stores the ServletConfig so that getServletConfig() and the helper method getInitParameter() work later. If you forget super.init, those calls return null and you will see a NullPointerException far from the actual cause.
The Single-Instance, Multi-Threaded Model
The container creates exactly one instance of your servlet class (by default) and routes all concurrent requests through that one object. Ten simultaneous users means ten threads all executing your doGet — on the same instance, reading and writing the same fields.
This design maximises throughput: object creation is expensive; thread creation is cheaper; sharing one servlet avoids both. But it shifts the thread-safety burden entirely onto you.
private int count = 0; with count++ inside doGet is a data race. Two threads can read the same value, both increment it, and write back the same result — silently losing one increment. Use AtomicInteger, LongAdder, or synchronisation as appropriate.
What Is and Is Not Safe in a Servlet
The critical rule is simple: distinguish between data that belongs to the servlet and data that belongs to a single request.
- Safe to store as instance fields: immutable objects (strings, primitive wrappers, final references set once in
init), thread-safe objects (AtomicInteger,ConcurrentHashMap), stateless helper objects (a JacksonObjectMapperconfigured at startup). - Never store as instance fields: per-request data such as a user's input, a
HttpServletRequestorHttpServletResponsereference, a result set, or any local computation. Declare those as local variables inside the method — each thread gets its own stack frame.
Lazy vs Eager Initialisation with load-on-startup
By default the container initialises a servlet on the first request it receives — lazy initialisation. That first caller pays the cost of init. For servlets with heavy startup work (building a cache, warming a connection pool) that delay is undesirable. The load-on-startup attribute forces eager initialisation at deploy time:
The integer value controls initialisation order when multiple servlets use loadOnStartup: servlet with value 1 inits before value 2, and so on. Negative values (or omitting the attribute) keep the default lazy behaviour.
init() override. HttpServlet provides a convenience no-arg init() that the container calls after the full init(ServletConfig) has stored the config. Overriding it means you do not have to remember super.init(config) — the config is already stored by the time your code runs.
The destroy Method in Practice
destroy is your cleanup hook, but it has limits. The container gives no hard guarantee on how long it will wait for destroy to complete before killing the JVM during an abrupt shutdown (SIGKILL, OOM kill). For critical shutdown work — flushing messages, completing financial transactions — use a JVM shutdown hook or a framework-level graceful shutdown mechanism as a second line of defence.
Also note: if init throws a ServletException, the container marks the servlet as unavailable and never calls destroy. Clean up anything partially initialised inside the same init method using a try/finally block.
Summary
The servlet lifecycle gives you three well-defined hooks: init (once, at startup), service/doGet/doPost (per request, on many threads), and destroy (once, at shutdown). The single-instance model means every instance field is shared across all concurrent requests — only thread-safe objects or final initialisation-time data belong there. Request-scoped data must live in local variables. Understanding this model is fundamental to writing correct, high-throughput web applications in Java.