Back to Data Management & Consistency
Q9Question 9 of 10

How do microservices maintain sessions when jumping between services

Key Concepts
  • Microservices stay stateless by having the client carry a signed JWT in the Authorization header on every request.
  • Each service validates the token locally by checking signature, expiry, issuer, and audience.
  • The token IS the session: identity travels with the request, not stored on any server.
  • Any instance of any service can handle any request because nothing is stored in instance memory.
  • Traditional server-side sessions break in microservices because a session in one instance is invisible to others.
  • The honest tradeoff is revocation: a JWT cannot be easily invalidated; use short-lived access tokens with refresh rotation.
  • For rare immediate-revocation needs, maintain a small denylist of revoked token IDs.

Definition

Microservices stay stateless by having the client carry a signed JWT in the Authorization header on every request, and each service validates it locally, so there is no shared session and any instance can serve any request.
  • Stateless authentication – The client carries a self-contained signed JWT on every request; no server stores session state.
  • Local validation – Each service checks the token's signature, expiry, issuer, and audience without calling the identity provider.
  • Token IS the session – The identity and roles that a traditional session stored on the server now travel inside the signed token.
  • Horizontal scalability – Any instance of any service handles any request because nothing is in instance memory.
  • Revocation tradeoff – A self-contained token is valid until expiry; mitigate with short-lived access tokens and refresh rotation.

Imagine Rajesh visits several departments at National Bank of India during one trip. In the old system, the receptionist keeps a note saying "Rajesh is inside", and every clerk phones the receptionist to confirm, which breaks when there are many branches. Instead the bank gives Rajesh a tamper-proof pass when he proves his identity, and he shows it at every department. Each department reads and verifies the pass itself, with no phone call to anyone.

Understanding the Concepts

Why do traditional sessions break in microservices?

A classic monolith stores a session in server memory. In microservices with many instances behind a load balancer, a session in one instance is invisible to the others.

JAVA
package com.bank.session;

public class SessionProblem {
    public static void main(String[] args) {
        System.out.println("Traditional session problem in microservices:");
        System.out.println("  Instance-1 creates session for Rajesh (sessionId=ABC)");
        System.out.println("  Next request hits Instance-2 via load balancer");
        System.out.println("  Instance-2 has NO session ABC -> 401 Unauthorized");
        System.out.println("  Rajesh appears logged out on every other request!");
        System.out.println();
        System.out.println("  Fix 1: Sticky sessions (defeats load balancing purpose)");
        System.out.println("  Fix 2: Shared session store in Redis (adds dependency)");
        System.out.println("  Fix 3: Stateless JWT (no server-side session at all) <- BEST");
    }
}
TEXT
Expected output:
Traditional session problem in microservices:
  Instance-1 creates session for Rajesh (sessionId=ABC)
  Next request hits Instance-2 via load balancer
  Instance-2 has NO session ABC -> 401 Unauthorized
  Rajesh appears logged out on every other request!

How does stateless JWT authentication work?

On login, the identity provider issues a signed JWT. The client stores it and sends it on every request. Each service validates locally.

JAVA
package com.bank.session;

public class StatelessJwtFlow {
    public static void main(String[] args) {
        System.out.println("Stateless JWT flow:");
        System.out.println("  1. POST /auth/login {user: rajesh, password: ****}");
        System.out.println("  2. Auth service verifies credentials");
        System.out.println("  3. Auth service mints JWT: {sub: rajesh, roles: [CUSTOMER], exp: 5min}");
        System.out.println("  4. Client stores JWT");
        System.out.println("  5. GET /accounts/SAV2001  Authorization: Bearer <jwt>");
        System.out.println("  6. Account service validates JWT locally (signature + expiry)");
        System.out.println("  7. POST /transfers       Authorization: Bearer <jwt>");
        System.out.println("  8. Transfer service validates same JWT locally");
        System.out.println();
        System.out.println("No server-side session. No shared state. Any instance handles any request.");
    }
}
TEXT
Expected output:
Stateless JWT flow:
  1. POST /auth/login {user: rajesh, password: ****}
  2. Auth service verifies credentials
  3. Auth service mints JWT: {sub: rajesh, roles: [CUSTOMER], exp: 5min}
  4. Client stores JWT
  5. GET /accounts/SAV2001  Authorization: Bearer <jwt>
  6. Account service validates JWT locally (signature + expiry)
  7. POST /transfers       Authorization: Bearer <jwt>
  8. Transfer service validates same JWT locally

No server-side session. No shared state. Any instance handles any request.

How does each service validate the token?

The service checks the signature using the public key, confirms expiry, and verifies issuer and audience.

JAVA
package com.bank.session;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class ServiceSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwkSetUri("https://auth.nbi.com/.well-known/jwks.json")
                ))
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated());
        return http.build();
    }
}
TEXT
Validation checks per request:
  1. Fetch public key from JWKS endpoint (cached)
  2. Verify signature: HMAC or RSA
  3. Check exp: not expired
  4. Check iss: matches expected issuer
  5. Check aud: matches this service
  6. Extract sub and roles -> set SecurityContext

Why does this scale?

Because everything needed is in the token. Any instance of any service validates locally. Add or remove instances freely.

JAVA
package com.bank.session;

public class WhyItScales {
    public static void main(String[] args) {
        System.out.println("Scalability with JWT:");
        System.out.println("  Account service: 5 instances");
        System.out.println("  Transfer service: 3 instances");
        System.out.println("  Statement service: 10 instances");
        System.out.println();
        System.out.println("  Rajesh's request can hit ANY instance of ANY service");
        System.out.println("  Because the JWT carries his identity");
        System.out.println("  No shared session store needed");
        System.out.println("  Scale up: just add instances. Scale down: just remove.");
    }
}
TEXT
Expected output:
Scalability with JWT:
  Account service: 5 instances
  Transfer service: 3 instances
  Statement service: 10 instances

  Rajesh's request can hit ANY instance of ANY service
  Because the JWT carries his identity
  No shared session store needed

What is the revocation tradeoff?

A JWT is self-contained and validated locally. There is no central place to invalidate it, so it stays valid until expiry.

JAVA
package com.bank.session;

public class RevocationTradeoff {
    public static void main(String[] args) {
        System.out.println("Revocation problem:");
        System.out.println("  Rajesh logs out, but his JWT has 5 minutes left");
        System.out.println("  An attacker with the stolen token can use it for 5 minutes");
        System.out.println();
        System.out.println("Mitigations:");
        System.out.println("  1. Short-lived access tokens: 5 minutes");
        System.out.println("  2. Refresh tokens: revoke on logout -> no new access tokens");
        System.out.println("  3. Refresh rotation: detect reuse -> revoke entire family");
        System.out.println("  4. Denylist: for immediate revocation, check jti against small list");
    }
}
TEXT
Expected output:
Revocation problem:
  Rajesh logs out, but his JWT has 5 minutes left
  An attacker with the stolen token can use it for 5 minutes

Mitigations:
  1. Short-lived access tokens: 5 minutes
  2. Refresh tokens: revoke on logout -> no new access tokens
  3. Refresh rotation: detect reuse -> revoke entire family
  4. Denylist: for immediate revocation, check jti against small list

How does RS256 help in multi-service systems?

The auth service signs with a private key. Every service verifies with the public key from a JWKS endpoint. Services cannot mint tokens.

JAVA
package com.bank.session;

public class Rs256Benefit {
    public static void main(String[] args) {
        System.out.println("RS256 in multi-service:");
        System.out.println("  Auth service: holds PRIVATE key, signs tokens");
        System.out.println("  Account service: fetches PUBLIC key from JWKS, verifies");
        System.out.println("  Transfer service: fetches PUBLIC key from JWKS, verifies");
        System.out.println();
        System.out.println("  Benefit: services CANNOT mint tokens (no private key)");
        System.out.println("  A compromised service can verify but not forge tokens");
    }
}
TEXT
Expected output:
RS256 in multi-service:
  Auth service: holds PRIVATE key, signs tokens
  Account service: fetches PUBLIC key from JWKS, verifies
  Transfer service: fetches PUBLIC key from JWKS, verifies

  Benefit: services CANNOT mint tokens (no private key)
  A compromised service can verify but not forge tokens

When would you use a shared session store instead?

Rarely, but if you need immediate revocation on every request, a shared store like Redis gives you that at the cost of a dependency.

JAVA
package com.bank.session;

public class WhenToUseRedisSession {
    public static void main(String[] args) {
        System.out.println("Use JWT (stateless) when:");
        System.out.println("  - Standard microservices architecture");
        System.out.println("  - Horizontal scaling is important");
        System.out.println("  - 5-minute token TTL is acceptable for revocation");
        System.out.println();
        System.out.println("Use Redis session (stateful) when:");
        System.out.println("  - Immediate revocation on EVERY request is required");
        System.out.println("  - Regulatory requirement for instant session kill");
        System.out.println("  - Accept the shared dependency and latency cost");
    }
}
TEXT
Expected output:
Use JWT (stateless) when:
  - Standard microservices architecture
  - Horizontal scaling is important
  - 5-minute token TTL is acceptable for revocation

Use Redis session (stateful) when:
  - Immediate revocation on EVERY request is required
  - Regulatory requirement for instant session kill
  - Accept the shared dependency and latency cost

What should a senior engineer know?

Never store sensitive data in the token. Always send over TLS. Validate audience to prevent cross-service replay. Propagate a correlation ID for tracing.

TEXT
Senior guidelines:
  [x] JWT for stateless, scalable authentication
  [x] RS256 so services verify without holding signing secret
  [x] Short-lived access tokens (5 min) + refresh rotation
  [x] Validate iss, aud, exp on every request
  [x] Never store sensitive data in payload (only encoded, not encrypted)
  [x] Always over TLS (bearer credential)
  [x] Propagate correlation/trace ID alongside token
  [x] Optional denylist for forced immediate revocation

Visual Overview

DIAGRAM
  Stateless Session with JWT:

  ┌──────────┐  login  ┌──────────┐
  │  Client  │────────▶│ Auth Svc │  verify credentials
  │ (mobile) │◀────────│          │  mint JWT
  └──────┬───┘  JWT    └──────────┘
         │
         │ Authorization: Bearer <jwt>
         │
    ┌────▼─────┐  validate  ┌──────────────┐
    │ Account  │  JWT       │ Instance 1   │ any instance
    │ Service  │  locally   │ Instance 2   │ serves any
    │          │            │ Instance 3   │ request
    └────┬─────┘            └──────────────┘
         │
         │ Authorization: Bearer <jwt> (same token)
         │
    ┌────▼─────┐  validate
    │ Transfer │  JWT
    │ Service  │  locally
    └──────────┘

  No server-side session anywhere.
  Token carries the identity.
  Any instance, any service.

Key Takeaway

Microservices do not maintain traditional server-side sessions. Instead, the client carries a signed JWT in the Authorization header on every request, and each service validates it locally by checking the signature, expiry, issuer, and audience. The token IS the session: the identity and roles that a server-side session would have stored now travel inside the signed token, so any instance of any service can handle any request.

The traditional model breaks in microservices because a session in one instance's memory is invisible to the others. A load balancer sends the next request to a different instance that has no session, so the user appears logged out. At National Bank of India, Rajesh was frustrated by intermittent logouts until Pankaj switched to stateless JWTs: Rajesh logged in once and was recognized on any instance of any service.

The honest tradeoff is revocation. A self-contained token is valid until it expires, so a stolen token is usable for its remaining lifetime. The mitigation is short-lived access tokens (5 minutes) paired with refresh tokens that are revoked on logout. For cases needing instant revocation, a small denylist of token IDs adds a little shared state back.

Remember one sentence: microservices stay stateless by having the client carry a signed JWT on every request and each service validate it locally, so there is no shared session, any instance serves any request, and the only tradeoff is revocation handled with short-lived tokens and refresh rotation.

---

Share
Use ← → arrow keys to navigate