The Fundamental Problem: HTTP is Stateless
To understand why sessions and cookies exist, you need to understand a fundamental characteristic of HTTP: it is inherently stateless. This means that HTTP treats every request as completely independent—the server has no built-in memory of previous requests from the same client.
This statelessness was actually a deliberate design decision that makes HTTP simple and scalable. But it creates a significant challenge: how do we create experiences that feel connected?
Why Statelessness is a Problem
Think about the things you do every day that require "memory":
- Staying logged in: You don't want to enter your password for every single page you visit
- Shopping carts: Adding items, browsing other pages, then checking out—the cart needs to persist
- Personalization: Showing your name, remembering your preferences, recent history
- Multi-step forms: Filling out a wizard where data from step 1 is needed in step 3
- Security: Knowing that the person making request #2 is the same person who authenticated in request #1
Without some mechanism to maintain state, every request would be like meeting someone with complete amnesia—no matter how many times you've talked before, you'd have to introduce yourself again.
Without state management, every request starts from zero:
- User logs in → Server forgets immediately after responding
- User adds item to cart → Server doesn't remember what's in the cart
- User fills out step 1 of a form → Step 2 doesn't know what happened in step 1
- Every request would need full re-authentication
Cookies and Sessions solve this problem! They provide mechanisms to maintain state across multiple requests.
Two Solutions to One Problem
There are two main approaches to maintaining state:
- Cookies: Store small pieces of data in the browser itself. The browser automatically sends this data with every request.
- Sessions: Store data on the server, and give the client a "ticket" (usually via a cookie) that identifies their session.
In practice, sessions and cookies often work together: a session ID is stored in a cookie, which the server uses to look up the full session data.
// Without sessions - server has no memory
Request 1: POST /login {username: "john", password: "***"}
Response: 200 OK "Login successful!"
Request 2: GET /my-profile
Response: 401 Unauthorized "Who are you?" ← Server forgot!
// With sessions - server remembers
Request 1: POST /login {username: "john", password: "***"}
Response: 200 OK, Set-Cookie: JSESSIONID=abc123 ← Server creates session
Request 2: GET /my-profile
Cookie: JSESSIONID=abc123 ← Browser sends cookie
Response: 200 OK {"name": "John Doe"} ← Server recognizes user!
Cookies Explained
Cookies are small pieces of data stored by the browser and sent with every request to the same domain.
How Cookies Work
// 1. Server sends Set-Cookie header in response
HTTP/1.1 200 OK
Set-Cookie: username=john; Path=/; Max-Age=86400
Set-Cookie: theme=dark; Path=/; HttpOnly
// 2. Browser stores the cookies
// 3. Browser sends cookies with EVERY subsequent request to that domain
GET /api/profile HTTP/1.1
Host: example.com
Cookie: username=john; theme=dark ← Automatically included!
Cookie Attributes
| Attribute | Example | Purpose |
|---|---|---|
Name=Value |
session=abc123 |
The actual data |
Domain |
Domain=example.com |
Which domain can receive the cookie |
Path |
Path=/api |
URL path scope |
Expires |
Expires=Thu, 01 Jan 2027 |
When to delete (absolute date) |
Max-Age |
Max-Age=86400 |
When to delete (seconds from now) |
Secure |
Secure |
Only send over HTTPS |
HttpOnly |
HttpOnly |
JavaScript cannot access |
SameSite |
SameSite=Strict |
CSRF protection |
Setting Cookies in Java
// Using Servlet API
@GetMapping("/login")
public void login(HttpServletResponse response) {
Cookie cookie = new Cookie("username", "john");
cookie.setMaxAge(60 * 60 * 24); // 24 hours in seconds
cookie.setPath("/"); // Available to entire site
cookie.setHttpOnly(true); // No JavaScript access
cookie.setSecure(true); // HTTPS only
response.addCookie(cookie);
}
// Using Spring ResponseCookie (more options)
@GetMapping("/login")
public ResponseEntity<String> login() {
ResponseCookie cookie = ResponseCookie.from("session", "abc123")
.httpOnly(true)
.secure(true)
.path("/")
.maxAge(Duration.ofHours(24))
.sameSite("Strict") // Strict, Lax, or None
.build();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body("Logged in!");
}
Reading Cookies in Java
// Using Servlet API
@GetMapping("/profile")
public String getProfile(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("username".equals(cookie.getName())) {
return "Hello, " + cookie.getValue();
}
}
}
return "Not logged in";
}
// Using Spring @CookieValue annotation
@GetMapping("/profile")
public String getProfile(
@CookieValue(name = "username", required = false) String username) {
if (username != null) {
return "Hello, " + username;
}
return "Not logged in";
}
Deleting Cookies
// To delete a cookie, set Max-Age to 0
@GetMapping("/logout")
public void logout(HttpServletResponse response) {
Cookie cookie = new Cookie("session", null);
cookie.setMaxAge(0); // Delete immediately
cookie.setPath("/"); // Must match original path!
response.addCookie(cookie);
}
// Using Spring
ResponseCookie cookie = ResponseCookie.from("session", "")
.maxAge(0)
.path("/")
.build();
Sessions Explained
Sessions store user data on the server, identified by a unique session ID sent via cookie.
// How sessions work:
┌─────────────────┐ ┌─────────────────┐
│ BROWSER │ │ SERVER │
└────────┬────────┘ └────────┬────────┘
│ │
│ 1. First Request (no cookie) │
│ ─────────────────────────────────> │
│ │
│ 2. Create session
│ ┌─────────────────────┐
│ │ Sessions Map: │
│ │ "abc123" → { │
│ │ user: null, │
│ │ cart: [] │
│ │ } │
│ └─────────────────────┘
│ │
│ 3. Response + Set-Cookie: JSESSIONID=abc123
│ <───────────────────────────────── │
│ │
│ 4. User logs in │
│ POST /login │
│ Cookie: JSESSIONID=abc123 │
│ ─────────────────────────────────> │
│ │
│ 5. Update session
│ ┌─────────────────────┐
│ │ Sessions Map: │
│ │ "abc123" → { │
│ │ user: "john", │
│ │ cart: [] │
│ │ } │
│ └─────────────────────┘
│ │
│ 6. Subsequent requests use same session
│ Cookie: JSESSIONID=abc123 │
│ ─────────────────────────────────> │
│ Server looks up "abc123" │
│ and knows who you are! │
▼ ▼
Using HttpSession in Java
// Storing data in session
@PostMapping("/login")
public String login(@RequestBody LoginRequest request,
HttpSession session) {
User user = authService.authenticate(request);
if (user != null) {
// Store user in session
session.setAttribute("user", user);
session.setAttribute("loginTime", LocalDateTime.now());
return "Login successful";
}
throw new AuthenticationException("Invalid credentials");
}
// Reading data from session
@GetMapping("/profile")
public User getProfile(HttpSession session) {
User user = (User) session.getAttribute("user");
if (user == null) {
throw new UnauthorizedException("Please log in");
}
return user;
}
// Shopping cart example
@PostMapping("/cart/add")
public Cart addToCart(@RequestBody CartItem item,
HttpSession session) {
@SuppressWarnings("unchecked")
List<CartItem> cart = (List<CartItem>) session.getAttribute("cart");
if (cart == null) {
cart = new ArrayList<>();
}
cart.add(item);
session.setAttribute("cart", cart);
return new Cart(cart);
}
// Logout - invalidate session
@PostMapping("/logout")
public String logout(HttpSession session) {
session.invalidate(); // Destroys the session
return "Logged out";
}
Session Configuration
// application.properties
server.servlet.session.timeout=30m # Session timeout
server.servlet.session.cookie.name=SESSIONID # Cookie name
server.servlet.session.cookie.http-only=true # HttpOnly flag
server.servlet.session.cookie.secure=true # Secure flag (HTTPS)
server.servlet.session.cookie.same-site=lax # SameSite policy
// Programmatic configuration
@Configuration
public class SessionConfig {
@Bean
public ServletContextInitializer servletContextInitializer() {
return servletContext -> {
servletContext.getSessionCookieConfig().setMaxAge(3600);
servletContext.getSessionCookieConfig().setHttpOnly(true);
servletContext.getSessionCookieConfig().setSecure(true);
};
}
}
Session vs Cookies Comparison
| Aspect | Cookies | Sessions |
|---|---|---|
| Storage Location | Browser (client-side) | Server (server-side) |
| Data Size | ~4KB per cookie | Limited by server memory |
| Security | Data visible to client | Data hidden on server |
| Scalability | No server memory needed | Requires session storage |
| Persistence | Can survive browser close | Usually lost on server restart |
| Use Case | Preferences, tokens | User data, shopping cart |
Use sessions for sensitive data (user info, permissions) and cookies for non-sensitive data (preferences, tracking). Never store passwords or sensitive info directly in cookies!
Session Storage Options
1. In-Memory (Default)
// Default - sessions stored in server memory
// Problem: Lost on restart, can't scale horizontally
Server A ─────────────────────────────────
│ Session: abc123 → {user: "john"}
│ Session: def456 → {user: "jane"}
// If user's next request goes to Server B...
Server B ─────────────────────────────────
│ No sessions! User appears logged out!
2. Database-Backed Sessions
// Store sessions in database (JDBC)
// pom.xml
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
</dependency>
// application.properties
spring.session.store-type=jdbc
spring.session.jdbc.initialize-schema=always
// That's it! Sessions now stored in database
// Works across multiple servers
3. Redis Sessions (Recommended for Scale)
// Store sessions in Redis (fast, distributed)
// pom.xml
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
// application.properties
spring.session.store-type=redis
spring.data.redis.host=localhost
spring.data.redis.port=6379
// Now all servers share the same session store!
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Server A │ │ Server B │ │ Server C │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└───────────────┼───────────────┘
│
┌──────┴──────┐
│ REDIS │
│ (Sessions) │
└─────────────┘
Security Considerations
Cookie Security Flags
// ALWAYS use these flags for session cookies!
ResponseCookie sessionCookie = ResponseCookie.from("JSESSIONID", sessionId)
// HttpOnly - Prevents JavaScript access (XSS protection)
.httpOnly(true)
// Secure - Only sent over HTTPS
.secure(true)
// SameSite - CSRF protection
// Strict: Never send with cross-site requests
// Lax: Send with top-level navigations
// None: Always send (requires Secure)
.sameSite("Strict")
.path("/")
.build();
Session Fixation Prevention
// Problem: Attacker sets session ID before user logs in
// Solution: Generate new session ID after login
@PostMapping("/login")
public String login(@RequestBody LoginRequest request,
HttpServletRequest httpRequest) {
User user = authService.authenticate(request);
if (user != null) {
// Invalidate old session and create new one
HttpSession oldSession = httpRequest.getSession(false);
if (oldSession != null) {
oldSession.invalidate();
}
// Create new session with new ID
HttpSession newSession = httpRequest.getSession(true);
newSession.setAttribute("user", user);
return "Login successful";
}
throw new AuthenticationException("Invalid credentials");
}
// Spring Security does this automatically!
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.sessionManagement(session -> session
.sessionFixation().newSession() // or .migrateSession()
);
return http.build();
}
}
Session Timeout
// Set appropriate timeout for your application
// application.properties
server.servlet.session.timeout=30m # 30 minutes
// Programmatically
HttpSession session = request.getSession();
session.setMaxInactiveInterval(30 * 60); // 30 minutes in seconds
// Check remaining time
long lastAccessed = session.getLastAccessedTime();
int maxInactive = session.getMaxInactiveInterval();
long timeRemaining = (lastAccessed + (maxInactive * 1000)) - System.currentTimeMillis();
Stateless Authentication (JWT Alternative)
For RESTful APIs and microservices, consider stateless authentication using JWT tokens instead of sessions.
// Session-based (Stateful)
┌────────────┐ ┌────────────┐
│ Client │ │ Server │
└─────┬──────┘ └─────┬──────┘
│ Cookie: JSESSIONID=abc │
│ ───────────────────────────> │
│ │
│ Look up session in memory/DB
│ Retrieve user data
│ │
│ 200 OK │
│ <─────────────────────────── │
// JWT-based (Stateless)
┌────────────┐ ┌────────────┐
│ Client │ │ Server │
└─────┬──────┘ └─────┬──────┘
│ Authorization: Bearer eyJ... │
│ ───────────────────────────> │
│ │
│ Verify JWT signature
│ Extract user from token payload
│ (No database lookup needed!)
│ │
│ 200 OK │
│ <─────────────────────────── │
| Aspect | Sessions | JWT |
|---|---|---|
| State | Server stores state | Token contains state |
| Scalability | Needs shared storage | Easily scalable |
| Revocation | Easy (delete session) | Harder (need blocklist) |
| Size | Small cookie | Larger token |
| Best For | Traditional web apps | APIs, microservices |
Working with Cookies in JavaScript
// Reading cookies (non-HttpOnly only)
function getCookie(name) {
const cookies = document.cookie.split('; ');
for (const cookie of cookies) {
const [key, value] = cookie.split('=');
if (key === name) {
return decodeURIComponent(value);
}
}
return null;
}
// Setting cookies
function setCookie(name, value, days) {
const expires = new Date();
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
document.cookie = `${name}=${encodeURIComponent(value)}; ` +
`expires=${expires.toUTCString()}; ` +
`path=/; ` +
`SameSite=Strict`;
}
// Deleting cookies
function deleteCookie(name) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
}
// Note: HttpOnly cookies are NOT accessible via JavaScript!
// This is intentional - it prevents XSS attacks from stealing session cookies
Common Patterns
"Remember Me" Functionality
@PostMapping("/login")
public ResponseEntity<?> login(
@RequestBody LoginRequest request,
@RequestParam(defaultValue = "false") boolean rememberMe,
HttpServletResponse response,
HttpSession session) {
User user = authService.authenticate(request);
session.setAttribute("user", user);
if (rememberMe) {
// Create persistent "remember me" token
String token = tokenService.createRememberMeToken(user);
ResponseCookie cookie = ResponseCookie.from("remember-me", token)
.httpOnly(true)
.secure(true)
.maxAge(Duration.ofDays(30)) // 30 days
.path("/")
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
}
return ResponseEntity.ok("Logged in");
}
// Check remember-me on startup
@Component
public class RememberMeFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("user") == null) {
// No active session, check for remember-me cookie
String rememberMeToken = getCookieValue(request, "remember-me");
if (rememberMeToken != null) {
User user = tokenService.validateRememberMeToken(rememberMeToken);
if (user != null) {
// Auto-login user
request.getSession(true).setAttribute("user", user);
}
}
}
chain.doFilter(request, response);
}
}
Best Practices
DO:
- Use HttpOnly for session cookies - Prevents XSS attacks
- Use Secure flag - Only transmit over HTTPS
- Set SameSite attribute - CSRF protection
- Regenerate session ID after login - Prevents session fixation
- Set appropriate timeouts - Balance security and UX
- Use external session store for scale - Redis or database
DON'T:
- Don't store sensitive data in cookies - Use sessions instead
- Don't trust cookie values - Always validate server-side
- Don't use predictable session IDs - Use cryptographic randomness
- Don't forget to invalidate sessions on logout
- Don't store passwords in sessions - Store user ID, fetch as needed
Summary
- Cookies: Small data stored in browser, sent with every request
- Sessions: Server-side storage identified by session ID cookie
- HttpOnly: Cookie flag that prevents JavaScript access
- Secure: Cookie flag that requires HTTPS
- SameSite: Cookie attribute for CSRF protection
- Session Fixation: Attack prevented by regenerating session ID
- Scaling: Use Redis or database for distributed sessions