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. Once you see it bare, the @SessionScope beans and "Spring
Security handles login" lines stop being magic and start being a mechanism you can reason about.
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 and the statelessness discussion in /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/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:
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).
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:
protected void
throws IOException
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:
protected void
throws IOException
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:
HttpSession session ;
if
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
JSESSIONIDbefore you log in can ride your authenticated session afterward. This is session fixation; the fix is a new id at the moment privilege changes. HttpOnlyandSecurecookies —HttpOnlykeeps JavaScript from reading the session cookie (so an XSS bug can't steal it);Securekeeps 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
- 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.
- The mechanism: the server creates a session and sends a session id in a
Set-Cookieheader; the browser returns it in theCookieheader on every later request; the server uses it to look up your data. The container's conventional cookie name isJSESSIONID. - 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.
- In code:
request.getSession()(orgetSession(false)to avoid creating one),setAttribute/getAttributeto store and read across requests,invalidate()on logout. The API handles the cookie and id for you. - ⚠️ 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.
- 💡 Frameworks (
@SessionScope, Spring Session, Spring Security) are built onHttpSession. Mind the security basics: regenerate the id on login (fixation),HttpOnly/Securecookies, and timeouts.
Quick check
Three questions on the ideas that have to stick:
[
{
"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."
}
]
← Phase 5: Filters & the Chain · Guide overview · Phase 7: From Servlets to Frameworks →
Check your understanding
1. When a browser keeps you 'logged in' across requests, where does your actual session data live?
2. What does request.getSession(false) do that request.getSession() does not?
3. Why do stateless tokens (like a JWT) scale across multiple servers more easily than server-side sessions?