Definition
- Cookie – a small piece of data (~4 KB max) set by the server via
Set-Cookieheader, stored on the client, and returned with every matching request; user-controllable. - Session – server-side state held in memory or a shared store, identified by an opaque session ID typically carried in a
JSESSIONIDcookie; data never leaves the server. - Security attributes –
HttpOnlyblocks JavaScript access,Securerestricts to HTTPS,SameSitedefends against CSRF. - Scaling – plain in-memory sessions do not survive across instances; use Spring Session with Redis or switch to stateless JWTs.
When Rajesh visits National Bank of India, the receptionist gives him a small token with a number on it: that is the cookie. Behind the counter the bank keeps a full file for that token number with Rajesh's logged-in status and pending transactions: that is the session. Rajesh carries only the token; the bank keeps the file. If he scribbled a bigger balance on his token it would not matter, because the truth lives in the bank's file.
Understanding the Concepts
What is a cookie?
A cookie is set by the server with a Set-Cookie response header and returned by the browser in the Cookie request header on every subsequent matching request. It is limited to ~4 KB.
// Server sets the cookie:
HTTP/1.1 200 OK
Set-Cookie: JSESSIONID=7723ABCD; HttpOnly; Secure; SameSite=Strict; Path=/
// Browser sends it back on every request:
GET /api/v1/accounts HTTP/1.1
Cookie: JSESSIONID=7723ABCD
Because it lives on the client, the user can read, edit, or delete it. Never store trusted data like a balance or a role directly in a plain cookie.
What are the security attributes of a cookie?
Each attribute closes a specific attack vector.
| Attribute | Purpose |
|---|---|
| HttpOnly | JavaScript cannot read the cookie, blocking XSS theft |
| Secure | Cookie is only sent over HTTPS |
| SameSite=Strict | Cookie is not sent on cross-site requests, defending against CSRF |
| Max-Age / Expires | Controls how long the cookie lives |
| Path | Limits which paths the cookie is sent on |
// Setting a secure cookie in Spring:
ResponseCookie cookie = ResponseCookie.from("JSESSIONID", sessionId)
.httpOnly(true)
.secure(true)
.sameSite("Strict")
.path("/")
.maxAge(Duration.ofHours(1))
.build();
response.addHeader("Set-Cookie", cookie.toString());
What is a session?
A session is server-side storage. When a user logs in, the server creates a session, stores the real data (user ID, roles, workflow state) in server memory, and sends back only an opaque session ID in a cookie.
package com.bank.controller;
import jakarta.servlet.http.HttpSession;
import java.util.List;
@PostMapping("/api/v1/login")
public LoginResponse login(@RequestBody LoginRequest req, HttpSession session) {
// Validate credentials...
session.setAttribute("userId", "rajesh"); // stays on the server
session.setAttribute("roles", List.of("CUSTOMER")); // stays on the server
return new LoginResponse("Login successful", session.getId());
}
@GetMapping("/api/v1/profile")
public ProfileResponse profile(HttpSession session) {
String userId = (String) session.getAttribute("userId"); // read from server
return profileService.getProfile(userId);
}
How do sessions scale across multiple instances?
Plain in-memory sessions do not survive across instances behind a load balancer. The solutions are sticky sessions (pin user to one server), a shared session store (Redis via Spring Session), or stateless JWTs.
// Spring Session with Redis – any instance can read the session:
// application.yml:
// spring.session.store-type: redis
// spring.data.redis.host: redis.bank.internal
@EnableRedisHttpSession
public class SessionConfig {
// Sessions are now stored in Redis, shared across all instances
}
Instance A ──→ Redis ←── Instance B
│
Session data lives here,
accessible by any instance
When do you use a cookie directly?
Use a cookie for small, non-sensitive preferences the client legitimately owns: chosen language, theme, or a signed token.
// Language preference in a cookie:
ResponseCookie lang = ResponseCookie.from("lang", "en")
.httpOnly(false) // JavaScript may read this
.secure(true)
.maxAge(Duration.ofDays(365))
.build();
When do you use a server session?
Use a session for sensitive or larger state: authenticated identity, shopping-cart workflow, multi-step form data. The data stays server-side where the user cannot tamper.
// Multi-step bill payment – state on the server:
session.setAttribute("billPayment", BillPayment.builder()
.billerCode("ELEC-001")
.amount(new BigDecimal("5000"))
.build());
// The client carries only the session ID; the payment details are safe on the server.
What about JWTs as an alternative to sessions?
For stateless REST APIs, many teams use a signed JWT instead of server sessions. The JWT carries claims without server-side storage, eliminating the need for session replication.
// JWT in a cookie – stateless, no server session needed:
ResponseCookie jwt = ResponseCookie.from("token", signedJwt)
.httpOnly(true)
.secure(true)
.sameSite("Strict")
.maxAge(Duration.ofMinutes(30))
.build();
The trade-off: JWTs cannot be revoked until they expire (unless you maintain a blocklist), while server sessions can be invalidated instantly.
What are common mistakes?
The most dangerous mistake is storing a role or balance directly in a plain cookie. The user can edit the cookie in developer tools.
// Wrong – storing role in a cookie:
ResponseCookie.from("role", "CUSTOMER").build();
// User edits cookie to "role=ADMIN" → privilege escalation!
// Correct – role stored in server session:
session.setAttribute("roles", List.of("CUSTOMER"));
// User carries only the opaque session ID.
Another mistake is not regenerating the session ID on login, which enables session fixation.
// Correct – regenerate session ID on login:
request.getSession().invalidate(); // destroy old session
HttpSession newSession = request.getSession(true); // create new ID
newSession.setAttribute("userId", "rajesh");
What should a senior engineer know?
The session ID is as powerful as a password while it is valid, so it must be protected like one. Mark the cookie HttpOnly, Secure, and SameSite. Regenerate the session ID on login. Expire sessions after inactivity. Session fixation and session hijacking are real threats in banking. For horizontal scaling, Spring Session with Redis is the standard approach. For stateless APIs, a signed JWT in an HttpOnly cookie avoids session replication entirely, but you lose instant revocation.
Visual Overview
Cookie vs Session:
Client (Browser)
┌────────────────────────────┐
│ Cookie: JSESSIONID=7723 │ ← small tag, client-side
│ (can be read/edited) │
└──────────┬─────────────────┘
│ sent with every request
▼
Server
┌────────────────────────────┐
│ Session store (Memory/Redis)
│ 7723 → { userId: rajesh, │ ← real data, server-side
│ roles: [CUSTOMER],│
│ workflow: ... } │
└────────────────────────────┘
Client carries the key; server holds the vault.
Key Takeaway
A cookie is a small piece of data that the server sets and the browser returns on every matching request. Because it lives on the client, the user can read and edit it, so you must never store trusted data like a balance or a role in a plain cookie. A session is server-side state: the server keeps the real data and sends back only an opaque session ID in a cookie. The sensitive data never travels to or is editable by the client.
Choose between them by sensitivity and size. Small non-sensitive preferences go in a cookie. Anything sensitive goes in a server session. For a horizontally scaled stateless REST API, a signed JWT in an HttpOnly cookie avoids session replication, and when server sessions are needed across instances, Spring Session with Redis is the standard approach.
At National Bank of India, an early version stored the user role directly in a cookie, and a customer simply edited it in developer tools to flip himself to admin. Moving all sensitive state into server sessions keyed by an opaque ID, with the cookie hardened as HttpOnly, Secure, and SameSite, and the session ID regenerated on login, closed that vulnerability.
The principle is that the session ID is as powerful as a password while it is valid. The client holds the key but never the vault. That separation is what lets you give users a smooth stateful experience without ever trusting the client with data a bank cannot afford to have tampered.
Remember one sentence: the cookie is the key the client carries, the session is the vault the server guards, and the two must never be confused.
---