Skip to main content

Session Management Service Design and Implementation

Comprehensive low-level design for a scalable session management service with token-based authentication, storage strategies, and security considerations.

A robust session management system is critical for modern web applications requiring secure user authentication and stateless session handling across distributed systems.

1. Problem Statement & Requirements

Clarifying questions

  1. What scale expectations are we targeting? What's the expected number of concurrent active sessions, peak requests per second, and total session count?
  2. What consistency requirements exist? Should session invalidation be immediate across all services (strong consistency) or eventual consistency acceptable?
  3. What are the key entities and their relationships? Is session tied to user, device, or application? How does session expiration interact with refresh tokens?
  4. What's the read/write ratio for sessions? Are we primarily reading session data or frequently creating/invalidating sessions?
  5. What are the latency sensitivity requirements? What's the acceptable P95/P99 response time for session validation operations?
  6. What authentication model is in use? Is it JWT-based, OAuth2, or session-cookie based? Are there external identity providers involved?
  7. What external integrations are required? Do we need to integrate with auth0, Okta, or other identity services for session validation?

Functional requirements

  • Create and validate user sessions
  • Invalidate sessions (logout, token expiration)
  • Extend session lifetime
  • Retrieve session details for authenticated requests
  • Handle session refresh tokens
  • Support multi-device session management
  • Provide session audit trail (login time, IP, device info)
  • Support session invalidation across distributed services

Non-functional requirements

  • Latency targets:
    • p50: <5ms for session validation
    • p95: <20ms for session validation
    • p99: <50ms for session validation
  • Throughput: 100K+ concurrent session operations per second
  • Availability: 99.9% uptime (438 minutes of downtime per year)
  • Session TTL: Default 24 hours, refreshable up to 7 days
  • Concurrent session limit: Max 10 active sessions per user

Constraints and assumptions

  • Sessions must be globally accessible across microservices in a distributed system
  • Session data should not be stored in user session cookies (security risk)
  • Sessions are stateful and must maintain metadata like IP, device, and login time
  • Session invalidation must be atomic across all services (eventual consistency acceptable for non-critical operations)
  • Service-to-service communication uses gRPC or HTTP/2 with TLS
  • No external session store (e.g., Redis) is available - must build from scratch

Out of scope

  • Session encryption or obfuscation (handled by transport layer)
  • Session sharing between users or devices
  • Session migration across authentication providers
  • Mobile push notification integration for session alerts
  • Session data encryption at rest (assumed to be handled by storage layer)
  • Cross-service session synchronization (e.g., invalidating sessions in other services)

2. API Design

API Style Selection

gRPC with Protocol Buffers is selected for the Session Management service due to:

  • Strong typing and automatic code generation
  • Efficient binary serialization (lower payload size than JSON)
  • Built-in support for streaming and bidirectional communication
  • Native support for versioning through proto files
  • Better performance characteristics for high-throughput scenarios

Endpoint Definitions

MethodPathDescriptionAuthRate Limit
CreateSession/v1/session/createCreates a new user sessionNone100 req/min
GetSession/v1/session/getRetrieves session by IDJWT1000 req/min
RefreshSession/v1/session/refreshExtends session validityJWT50 req/min
InvalidateSession/v1/session/invalidateInvalidates a sessionJWT50 req/min
InvalidateAllSessions/v1/session/invalidate/allInvalidates all user sessionsJWT10 req/min
ListActiveSessions/v1/session/listLists all active sessions for userJWT20 req/min

Request and Response Schemas

CreateSession Request:

message CreateSessionRequest {
  string user_id = 1;
  string device_info = 2;
  string ip_address = 3;
  int64 expiry_time = 4;
}

CreateSession Response:

message CreateSessionResponse {
  string session_id = 1;
  string access_token = 2;
  int64 expiry_time = 3;
  string refresh_token = 4;
}

GetSession Request:

message GetSessionRequest {
  string session_id = 1;
}

GetSession Response:

message GetSessionResponse {
  string session_id = 1;
  string user_id = 2;
  string device_info = 3;
  string ip_address = 4;
  int64 created_at = 5;
  int64 expiry_time = 6;
  bool is_active = 7;
}

RefreshSession Request:

message RefreshSessionRequest {
  string refresh_token = 1;
}

RefreshSession Response:

message RefreshSessionResponse {
  string session_id = 1;
  string access_token = 2;
  int64 expiry_time = 3;
}

HTTP Status Codes and Error Response Format

Status CodeDescription
200Success
400Bad Request - Invalid input
401Unauthorized - Invalid or missing auth
403Forbidden - Insufficient permissions
429Too Many Requests - Rate limit exceeded
500Internal Server Error

Standardized error response:

{
  "error": {
    "code": "SESSION_NOT_FOUND",
    "message": "Session with ID abc123 not found",
    "details": {
      "session_id": "abc123"
    }
  }
}

API Versioning Strategy

Versioned using path prefix: /v1/session/... with protobuf versioning in .proto files.

Pagination Design

Cursor-based pagination is used for session listing to:

  • Provide stable pagination across concurrent writes
  • Avoid issues with items being reordered during pagination
  • Support efficient querying of large session sets

Authentication and Authorization

All endpoints except CreateSession require JWT Bearer token authentication.

  • CreateSession: No auth required (initial session creation)
  • Other endpoints: JWT with user_id claim matching session owner
  • Session tokens are validated using HS256 signing

Rate Limiting Strategy

Rate limiting implemented at service level:

  • CreateSession: 100 req/min (no auth)
  • GetSession: 1000 req/min (JWT required)
  • RefreshSession: 50 req/min (JWT required)
  • InvalidateSession: 50 req/min (JWT required)
  • InvalidateAllSessions: 10 req/min (JWT required)
  • ListActiveSessions: 20 req/min (JWT required)

Idempotency Design

For mutation endpoints, idempotency keys are supported:

  • X-Idempotency-Key header in requests
  • Service stores last result for same key within 1 hour window
  • For session invalidation, idempotency ensures duplicate calls don't cause errors

3. Data Model Design

Entity-Relationship Diagram

erDiagram
    USER {
        string id PK
        string email
        string hashed_password
        boolean is_active
        timestamp created_at
        timestamp updated_at
    }

    SESSION {
        string id PK
        string user_id FK
        string token_hash
        timestamp expires_at
        timestamp created_at
        timestamp last_accessed_at
        string ip_address
        string user_agent
        boolean is_active
    }

    USER_DEVICE {
        string id PK
        string user_id FK
        string device_fingerprint
        timestamp created_at
        timestamp last_used_at
    }

    USER_DEVICE_SESSION {
        string user_device_id FK
        string session_id FK
        timestamp created_at
    }

    USER ||--o{ SESSION : "has"
    USER ||--o{ USER_DEVICE : "has"
    USER_DEVICE ||--o{ USER_DEVICE_SESSION : "has"
    SESSION ||--o{ USER_DEVICE_SESSION : "associated_with"

Table / Collection Schema Definitions

Users Table

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    hashed_password TEXT NOT NULL,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);

Sessions Table

CREATE TABLE sessions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token_hash VARCHAR(255) UNIQUE NOT NULL,
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    last_accessed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    ip_address INET,
    user_agent TEXT,
    is_active BOOLEAN DEFAULT TRUE,
    
    -- Ensure session expiration is in the future
    CONSTRAINT chk_session_expires_at CHECK (expires_at > created_at)
);

CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_token_hash ON sessions(token_hash);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
CREATE INDEX idx_sessions_last_accessed_at ON sessions(last_accessed_at);

User Devices Table

CREATE TABLE user_devices (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    device_fingerprint VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    last_used_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    -- Ensure uniqueness per user
    CONSTRAINT chk_unique_device_per_user UNIQUE (user_id, device_fingerprint)
);

CREATE INDEX idx_user_devices_user_id ON user_devices(user_user_id);
CREATE INDEX idx_user_devices_fingerprint ON user_devices(device_fingerprint);

User Device Sessions Junction Table

CREATE TABLE user_device_sessions (
    user_device_id UUID NOT NULL REFERENCES user_devices(id) ON DELETE CASCADE,
    session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    PRIMARY KEY (user_device_id, session_id)
);

CREATE INDEX idx_user_device_sessions_session_id ON user_device_sessions(session_id);

Primary Key Strategy

UUIDs for all primary keys - Justification:

  • Scalability: UUIDs provide globally unique identifiers, allowing for horizontal sharding without key conflicts
  • Security: Random UUIDs prevent enumeration attacks that could occur with auto-increment IDs
  • Decoupling: Enables independent database clusters and cross-service references
  • No contention: Eliminates the need for centralized ID generation services or sequence contention

Index Design

  1. Sessions Table:

    • idx_sessions_user_id: For user-based session lookups (e.g., session listing by user)
    • idx_sessions_token_hash: For token-based session retrieval (login, validation)
    • idx_sessions_expires_at: For session cleanup jobs and expiration checks
    • idx_sessions_last_accessed_at: For session activity tracking and rotation
  2. Users Table:

    • idx_users_email: For email-based authentication lookups
  3. User Devices Table:

    • idx_user_devices_user_id: For device-based session management
    • idx_user_devices_fingerprint: For device fingerprint lookups

Data Partitioning / Sharding Strategy

Shard by User ID (UUID) - Justification:

  • Consistent hashing: UUIDs can be hashed consistently to ensure predictable sharding
  • User-centric operations: Most queries are user-bound, making user ID a natural shard key
  • Cross-session consistency: Session management for a single user stays within one shard
  • Horizontal scaling: Allows for independent scaling of user sessions

Soft Delete vs. Hard Delete Decision

Soft delete with is_active flag - Justification:

  • Auditability: Maintains historical session data for security audits and compliance
  • Recovery capability: Allows for session reactivation if needed (e.g., in case of false positives)
  • Data integrity: Prevents orphaned references in junction tables
  • Compliance: Meets GDPR and other regulations that require data retention for audit trails

Audit Trail Design

Audit fields in all tables:

  • created_at: Timestamp of creation
  • updated_at: Timestamp of last modification (for users table)
  • last_accessed_at: For sessions, tracks last activity for rotation logic
  • created_by, updated_by (to be added) - For system-level audit tracking

Data Archival Strategy and Retention Policy

Archival strategy:

  • Sessions older than 30 days with is_active = FALSE are archived to cold storage (e.g., S3)
  • Archival process runs daily via scheduled job
  • Archive metadata stored in separate table with reference to original session ID

Retention policy:

  • Active sessions: Retained indefinitely (as long as user is active)
  • Inactive sessions: Retained for 30 days before archival
  • User account deletion: Sessions deleted immediately upon user soft-delete
-- Example archival table structure (for reference)
CREATE TABLE session_archives (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL,
    token_hash VARCHAR(255),
    expires_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE,
    last_accessed_at TIMESTAMP WITH TIME ZONE,
    ip_address INET,
    user_agent TEXT,
    archived_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    archive_location TEXT -- S3 path or other storage reference
);

4. Class Design & Service Layer

classDiagram
    class SessionService {
        +createSession(CreateSessionRequest) Session
        +getSession(String sessionId) Session
        +refreshSession(String sessionId, String refreshToken) Session
        +invalidateSession(String sessionId) void
        +invalidateUserSessions(String userId) void
        +validateSession(String sessionId) ValidationResult
    }
    
    class Session {
        -sessionId: String
        -userId: String
        -refreshToken: String
        -accessToken: String
        -expiresAt: Instant
        -refreshExpiresAt: Instant
        -status: SessionStatus
        -createdAt: Instant
        -updatedAt: Instant
    }
    
    class SessionRepository {
        +save(Session) void
        +findById(String sessionId) Optional~Session~
        +findByUserId(String userId) List~Session~
        +update(Session) void
        +delete(String sessionId) void
    }
    
    class TokenGenerator {
        +generateAccessToken(Session) String
        +generateRefreshToken() String
    }
    
    class SessionValidator {
        +validate(Session) ValidationResult
        +isTokenValid(String token) boolean
    }
    
    class EventPublisher {
        +publish(SessionEvent) void
    }
    
    class SessionEvent {
        -eventType: String
        -sessionId: String
        -userId: String
        -timestamp: Instant
    }
    
    class SessionStatus {
        ACTIVE
        EXPIRED
        INVALIDATED
    }
    
    class ValidationResult {
        -isValid: boolean
        -reason: String
    }
    
    SessionService --> Session : aggregates
    SessionService --> SessionRepository : uses
    SessionService --> TokenGenerator : uses
    SessionService --> SessionValidator : uses
    SessionService --> EventPublisher : publishes
    
    SessionValidator --> Session : validates
    SessionRepository --> Session : manages
    
    Session *-- SessionStatus : has
    Session *-- ValidationResult : returns
stateDiagram-v2
    [*] --> Active
    Active --> Expired : token expiry
    Active --> Invalidated : manual invalidation
    Expired --> [*]
    Invalidated --> [*]

Service Class Structure and Responsibilities

The SessionService is the core business logic layer that handles session lifecycle operations. It orchestrates interactions between the data repository, token generation, validation, and event publishing components.

Key responsibilities:

  • Session creation with proper token generation
  • Session validation and refresh operations
  • Session invalidation (both per-session and user-wide)
  • Event-driven session state changes
  • Business rule enforcement for session security

Interface / Abstract Class Design

public interface SessionRepository {
    void save(Session session);
    Optional<Session> findById(String sessionId);
    List<Session> findByUserId(String userId);
    void update(Session session);
    void delete(String sessionId);
}

public interface TokenGenerator {
    String generateAccessToken(Session session);
    String generateRefreshToken();
}

public interface SessionValidator {
    ValidationResult validate(Session session);
    boolean isTokenValid(String token);
}

Business Logic Encapsulation Patterns

Strategy Pattern: Session validation uses a strategy pattern to support different validation rules based on session type or security requirements.

public interface ValidationStrategy {
    ValidationResult validate(Session session);
}

public class SessionValidationContext {
    private final List<ValidationStrategy> strategies;
    
    public SessionValidationContext(List<ValidationStrategy> strategies) {
        this.strategies = strategies;
    }
    
    public ValidationResult validate(Session session) {
        for (ValidationStrategy strategy : strategies) {
            ValidationResult result = strategy.validate(session);
            if (!result.isValid()) {
                return result;
            }
        }
        return new ValidationResult(true, "Session valid");
    }
}

public class ExpiryValidationStrategy implements ValidationStrategy {
    @Override
    public ValidationResult validate(Session session) {
        if (Instant.now().isAfter(session.getExpiresAt())) {
            return new ValidationResult(false, "Session expired");
        }
        return new ValidationResult(true, "Valid");
    }
}

public class StatusValidationStrategy implements ValidationStrategy {
    @Override
    public ValidationResult validate(Session session) {
        if (session.getStatus() == SessionStatus.INVALIDATED) {
            return new ValidationResult(false, "Session invalidated");
        }
        return new ValidationResult(true, "Valid");
    }
}

Critical-Path Method Implementation

@Transactional
public Session placeOrder(String userId, OrderRequest orderRequest) {
    // Transaction boundary: entire session creation and validation
    String sessionId = UUID.randomUUID().toString();
    
    // Generate tokens with proper security
    String refreshToken = tokenGenerator.generateRefreshToken();
    String accessToken = tokenGenerator.generateAccessToken(
        Session.builder()
            .sessionId(sessionId)
            .userId(userId)
            .build()
    );
    
    // Create session object with proper expiration times
    Instant now = Instant.now();
    Session session = Session.builder()
        .sessionId(sessionId)
        .userId(userId)
        .refreshToken(refreshToken)
        .accessToken(accessToken)
        .expiresAt(now.plus(Duration.ofHours(1)))
        .refreshExpiresAt(now.plus(Duration.ofDays(30)))
        .status(SessionStatus.ACTIVE)
        .createdAt(now)
        .updatedAt(now)
        .build();
    
    // Validate session before saving (optimistic locking)
    ValidationResult validation = sessionValidator.validate(session);
    if (!validation.isValid()) {
        throw new SessionValidationException(validation.getReason());
    }
    
    // Save session to database
    sessionRepository.save(session);
    
    // Publish session created event for audit and async processing
    SessionEvent event = SessionEvent.builder()
        .eventType("SESSION_CREATED")
        .sessionId(sessionId)
        .userId(userId)
        .timestamp(Instant.now())
        .build();
    eventPublisher.publish(event);
    
    return session;
}

Transaction Boundary Design

A single transaction encompasses:

  1. Session creation and persistence
  2. Token generation (with proper security)
  3. Event publishing (ensured atomic with session save)
  4. Validation checks before persistence

Service Interface Contracts

public interface SessionService {
    Session createSession(CreateSessionRequest request);
    Session getSession(String sessionId);
    Session refreshSession(String sessionId, String refreshToken);
    void invalidateSession(String sessionId);
    void invalidateUserSessions(String userId);
    ValidationResult validateSession(String sessionId);
}

Saga / Orchestration Pattern

For distributed session management, a saga pattern is used for cross-service operations:

public class SessionSaga {
    private final List<SagaStep> steps;
    
    public void execute(SessionOperation operation) {
        for (SagaStep step : steps) {
            try {
                step.execute();
                // Update saga state
            } catch (Exception e) {
                handleFailure(step, e);
                break;
            }
        }
    }
    
    private void handleFailure(SagaStep failedStep, Exception error) {
        // Compensate previous steps
        for (int i = steps.indexOf(failedStep) - 1; i >= 0; i--) {
            steps.get(i).compensate();
        }
    }
}

Domain Object / Value Object Design

@Value
public class Session {
    String sessionId;
    String userId;
    String refreshToken;
    String accessToken;
    Instant expiresAt;
    Instant refreshExpiresAt;
    SessionStatus status;
    Instant createdAt;
    Instant updatedAt;
    
    public static class Builder {
        private String sessionId;
        private String userId;
        private String refreshToken;
        private String accessToken;
        private Instant expiresAt;
        private Instant refreshExpiresAt;
        private SessionStatus status;
        private Instant createdAt;
        private Instant updatedAt;
        
        public Builder sessionId(String sessionId) { this.sessionId = sessionId; return this; }
        public Builder userId(String userId) { this.userId = userId; return this; }
        public Builder refreshToken(String refreshToken) { this.refreshToken = refreshToken; return this; }
        public Builder accessToken(String accessToken) { this.accessToken = accessToken; return this; }
        public Builder expiresAt(Instant expiresAt) { this.expiresAt = expiresAt; return this; }
        public Builder refreshExpiresAt(Instant refreshExpiresAt) { this.refreshExpiresAt = refreshExpiresAt; return this; }
        public Builder status(SessionStatus status) { this.status = status; return this; }
        public Builder createdAt(Instant createdAt) { this.createdAt = createdAt; return this; }
        public Builder updatedAt(Instant updatedAt) { this.updatedAt = updatedAt; return this; }
        
        public Session build() {
            return new Session(sessionId, userId, refreshToken, accessToken, 
                              expiresAt, refreshExpiresAt, status, createdAt, updatedAt);
        }
    }
}

public enum SessionStatus {
    ACTIVE,
    EXPIRED,
    INVALIDATED
}

5. Concurrency & Thread Safety

Concurrency Model

The session management service operates in a multi-threaded environment with concurrent user requests and background maintenance tasks. The primary concurrency model leverages:

  • Java's thread pool executor for handling concurrent session operations (login, refresh, logout)
  • ReentrantReadWriteLock for protecting shared session state in memory
  • JVM-level synchronization for atomic operations on session counters and metadata
  • Async/Await pattern for non-blocking I/O operations (session token generation, cache updates)

Race Condition Prevention Strategy

Race conditions are mitigated through:

  1. Atomic operations for session counter increments and decrement operations
  2. Lock-based synchronization for critical session state updates (token refresh, session invalidation)
  3. Compare-and-swap (CAS) operations for optimistic updates to session metadata
  4. Thread-local storage for session context in request processing pipeline

Optimistic vs. Pessimistic Locking Selection

Optimistic locking is selected for session management because:

  • Session operations are typically read-heavy with infrequent writes
  • Concurrent login/logout operations are rare and can be handled gracefully with retries
  • Pessimistic locking would introduce unnecessary contention and reduce throughput
  • Session tokens are immutable once created, so concurrent reads are safe

Implementation approach:

// Optimistic locking for session updates
public boolean updateSession(Session session, SessionUpdateRequest request) {
    int expectedVersion = session.getVersion();
    Session updatedSession = session.withUpdates(request);
    
    // Use CAS to ensure no concurrent modification
    if (sessionRepository.updateIfVersionMatches(session.getId(), updatedSession, expectedVersion)) {
        // Update successful - publish session update event
        eventPublisher.publish(new SessionUpdatedEvent(session.getId(), updatedSession));
        return true;
    }
    
    // Version mismatch - retry or throw exception
    throw new ConcurrentModificationException("Session was modified by another process");
}

Deadlock Prevention Strategy

Deadlock prevention is achieved through:

  1. Lock ordering - All session operations acquire locks in consistent order (by session ID hash)
  2. Timeout-based lock acquisition - 50ms timeout for lock acquisition to prevent indefinite waits
  3. Hierarchical lock management - Session-level locks are acquired before global cache locks
  4. No circular dependency patterns in session state updates

Idempotency Implementation for Concurrent Duplicate Requests

Idempotency is handled at multiple levels:

  1. Request deduplication using request ID and timestamp in a distributed cache
  2. Token-based idempotency keys for session refresh operations
  3. Database-level uniqueness constraints on session tokens and user IDs
public class SessionIdempotencyService {
    private final RedisTemplate<String, String> redisTemplate;
    
    public boolean isIdempotentRequest(String requestId, long timestamp) {
        String key = "idempotency:" + requestId;
        String value = "processed:" + timestamp;
        
        // Use SET with NX (not exists) for atomic idempotency check
        Boolean isSet = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofMinutes(5));
        return isSet != null && isSet;
    }
    
    public void handleIdempotentOperation(String requestId, Runnable operation) {
        if (isIdempotentRequest(requestId, System.currentTimeMillis())) {
            operation.run();
        }
    }
}

Thread Safety in Session Cache Operations

Session cache operations are made thread-safe through:

  1. ConcurrentHashMap for in-memory session storage with atomic operations
  2. Read-write locks to allow concurrent reads while protecting writes
  3. Immutable session objects to prevent modification during read operations
  4. Copy-on-write approach for session metadata updates
public class ThreadSafeSessionCache {
    private final ConcurrentHashMap<String, Session> sessionStore = new ConcurrentHashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public Session getSession(String sessionId) {
        return sessionStore.get(sessionId);
    }
    
    public void putSession(Session session) {
        lock.writeLock().lock();
        try {
            sessionStore.put(session.getId(), session);
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public boolean removeSession(String sessionId) {
        lock.writeLock().lock();
        try {
            return sessionStore.remove(sessionId) != null;
        } finally {
            lock.writeLock().unlock();
        }
    }
}

6. Sequence Diagrams for Key Flows

sequenceDiagram
    participant U as User
    participant Svc as SessionService
    participant DB as Database
    participant Cache as Redis

    U->>Svc: Login Request (username, password)
    Svc->>DB: Validate credentials
    DB-->>Svc: User details
    Svc->>Cache: Store session key
    Svc->>DB: Create session record
    DB-->>Svc: Session ID
    Svc-->>U: Auth Token (JWT)
sequenceDiagram
    participant U as User
    participant Svc as SessionService
    participant DB as Database
    participant Cache as Redis

    U->>Svc: Request with Auth Token
    Svc->>Cache: Check session validity
    Cache-->>Svc: Session found/invalid
    alt Session valid
        Svc->>DB: Fetch session details
        DB-->>Svc: Session data
        Svc-->>U: Request processed
    else Session invalid/expired
        Svc-->>U: 401 Unauthorized
    end
sequenceDiagram
    participant U as User
    participant Svc as SessionService
    participant DB as Database
    participant Cache as Redis
    participant MQ as MessageQueue

    U->>Svc: Logout Request
    Svc->>Cache: Invalidate session key
    Svc->>DB: Mark session as inactive
    DB-->>Svc: Session updated
    Svc->>MQ: Publish session invalidation event
    MQ-->>Svc: Event acknowledged
    Svc-->>U: 200 OK

7. Caching Design

Cache Strategy Selection

For session management, we'll use cache-aside pattern with write-through behavior for critical session data and write-behind for audit trail data. This approach provides the best balance between consistency, performance, and operational simplicity.

Cache Key Design

Cache keys will follow a structured namespace pattern:

session:{sessionId}
session:user:{userId}
session:token:{tokenHash}
session:ip:{ipAddress}:{sessionId}

TTL Configuration

Data TypeTTLReason
Active Sessions30 minutesSession timeout after inactivity
User Sessions24 hoursKeep user session for re-authentication
Session Tokens1 hourToken expiration for security
IP-based sessions5 minutesShort-lived for IP tracking

Cache Eviction Policy

We'll implement a Least Recently Used (LRU) eviction policy with TTL-based expiration as secondary mechanism. This ensures:

  • Memory efficiency through LRU eviction
  • Automatic cleanup of stale sessions
  • Predictable cache behavior

Cache Stampede Prevention

To prevent cache stampedes:

  1. Probabilistic early expiry: Set cache TTL to 80% of actual expiration time
  2. Distributed locks: Use Redis distributed locks for cache population
  3. Cache warming: Pre-populate frequently accessed session data

Distributed Cache Architecture

We'll use Redis Cluster with:

  • 3 master nodes for session data
  • 3 replica nodes for read scaling
  • Redis Sentinel for high availability
  • Memory policy: allkeys-lru with maxmemory set to 2GB per node

Cache Invalidation Strategy

We'll implement event-driven invalidation:

  1. Session expiration → Immediate cache removal
  2. Session logout → Cache invalidation via event
  3. User session update → Cache refresh with new data
  4. Token rotation → Cache invalidation for old tokens
@Component
public class SessionCacheService {
    private final RedisTemplate<String, Session> redisTemplate;
    private final ObjectMapper objectMapper;
    
    public void invalidateSession(String sessionId) {
        String key = "session:" + sessionId;
        redisTemplate.delete(key);
        
        // Also invalidate related keys
        String userKey = "session:user:" + getSessionUserId(sessionId);
        redisTemplate.delete(userKey);
        
        // Publish cache invalidation event
        publishCacheInvalidationEvent(sessionId);
    }
    
    public Session getSession(String sessionId) {
        String key = "session:" + sessionId;
        
        // Check cache with optimistic locking
        Session session = redisTemplate.opsForValue().get(key);
        
        if (session == null) {
            // Cache miss - fetch from DB and populate cache
            session = sessionRepository.findById(sessionId);
            if (session != null) {
                // Use Redis transaction for consistency
                redisTemplate.multi();
                redisTemplate.opsForValue().set(key, session, Duration.ofMinutes(30));
                redisTemplate.opsForValue().set("session:user:" + session.getUserId(), 
                    session, Duration.ofHours(24));
                redisTemplate.exec();
            }
        }
        
        return session;
    }
}

8. Repository / Data Access Layer

Repository Pattern Implementation

The session management service follows the repository pattern to abstract data access logic and provide a clean interface for domain operations. Each entity has its own repository interface with standardized CRUD operations.

public interface SessionRepository {
    Optional<Session> findById(String sessionId);
    List<Session> findByUserId(String userId);
    Session save(Session session);
    void delete(String sessionId);
    boolean exists(String sessionId);
    long countByUserId(String userId);
}

ORM vs. Raw Query Decisions

For session management, we use raw SQL queries for the following reasons:

  • High-performance read/write operations with minimal overhead
  • Fine-grained control over query execution and optimization
  • Session data is relatively simple with predictable access patterns
  • Reduced ORM complexity for a high-throughput system
@Repository
public class SessionRepositoryImpl implements SessionRepository {
    
    private final JdbcTemplate jdbcTemplate;
    private final ObjectMapper objectMapper;
    
    public SessionRepositoryImpl(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
        this.jdbcTemplate = jdbcTemplate;
        this.objectMapper = objectMapper;
    }
    
    @Override
    public Optional<Session> findById(String sessionId) {
        String sql = "SELECT * FROM sessions WHERE session_id = ? AND expires_at > NOW()";
        try {
            return Optional.of(jdbcTemplate.queryForObject(sql, new SessionRowMapper(), sessionId));
        } catch (EmptyResultDataAccessException e) {
            return Optional.empty();
        }
    }
    
    @Override
    public List<Session> findByUserId(String userId) {
        String sql = "SELECT * FROM sessions WHERE user_id = ? AND expires_at > NOW() ORDER BY created_at DESC";
        return jdbcTemplate.query(sql, new SessionRowMapper(), userId);
    }
    
    @Override
    public Session save(Session session) {
        String sql = """
            INSERT INTO sessions (session_id, user_id, data, created_at, expires_at, last_accessed)
            VALUES (?, ?, ?, ?, ?, ?)
            ON DUPLICATE KEY UPDATE
                data = VALUES(data),
                expires_at = VALUES(expires_at),
                last_accessed = VALUES(last_accessed)
            """;
        
        jdbcTemplate.update(sql, 
            session.getId(),
            session.getUserId(),
            objectMapper.writeValueAsString(session.getData()),
            session.getCreatedAt(),
            session.getExpiresAt(),
            session.getLastAccessed()
        );
        
        return session;
    }
    
    @Override
    public void delete(String sessionId) {
        String sql = "DELETE FROM sessions WHERE session_id = ?";
        jdbcTemplate.update(sql, sessionId);
    }
    
    @Override
    public boolean exists(String sessionId) {
        String sql = "SELECT COUNT(*) FROM sessions WHERE session_id = ?";
        return jdbcTemplate.queryForObject(sql, Integer.class, sessionId) > 0;
    }
    
    @Override
    public long countByUserId(String userId) {
        String sql = "SELECT COUNT(*) FROM sessions WHERE user_id = ? AND expires_at > NOW()";
        return jdbcTemplate.queryForObject(sql, Long.class, userId);
    }
}

Read vs. Write Separation

We implement CQRS pattern for session management to separate read and write operations:

public interface SessionReadRepository {
    Optional<Session> findById(String sessionId);
    List<Session> findByUserId(String userId);
    long countByUserId(String userId);
}

public interface SessionWriteRepository {
    Session save(Session session);
    void delete(String sessionId);
    boolean exists(String sessionId);
}

Connection Pool Configuration

# application.yml
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      validation-timeout: 5000
      leak-detection-threshold: 60000

N+1 Query Prevention Strategy

To prevent N+1 query issues, we implement batch operations:

@Repository
public class BatchSessionRepository {
    
    public List<Session> findByIds(List<String> sessionIds) {
        if (sessionIds.isEmpty()) return Collections.emptyList();
        
        String placeholders = String.join(",", Collections.nCopies(sessionIds.size(), "?"));
        String sql = "SELECT * FROM sessions WHERE session_id IN (" + placeholders + ") AND expires_at > NOW()";
        
        return jdbcTemplate.query(sql, sessionIds.toArray(), new SessionRowMapper());
    }
    
    public List<Session> findByUserIds(List<String> userIds) {
        if (userIds.isEmpty()) return Collections.emptyList();
        
        String placeholders = String.join(",", Collections.nCopies(userIds.size(), "?"));
        String sql = "SELECT * FROM sessions WHERE user_id IN (" + placeholders + ") AND expires_at > NOW() ORDER BY created_at DESC";
        
        return jdbcTemplate.query(sql, userIds.toArray(), new SessionRowMapper());
    }
}

Batch Operation Design

public class BatchSessionOperations {
    
    public void deleteExpiredSessions() {
        String sql = "DELETE FROM sessions WHERE expires_at < NOW()";
        jdbcTemplate.update(sql);
    }
    
    public void updateLastAccessed(List<String> sessionIds) {
        String sql = "UPDATE sessions SET last_accessed = NOW() WHERE session_id IN (" + 
            String.join(",", Collections.nCopies(sessionIds.size(), "?")) + ")";
        
        jdbcTemplate.update(sql, sessionIds.toArray());
    }
}

Session Row Mapper

public class SessionRowMapper implements RowMapper<Session> {
    private final ObjectMapper objectMapper;
    
    public SessionRowMapper(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }
    
    @Override
    public Session mapRow(ResultSet rs, int rowNum) throws SQLException {
        try {
            String dataJson = rs.getString("data");
            Map<String, Object> data = dataJson != null ? 
                objectMapper.readValue(dataJson, new TypeReference<Map<String, Object>>() {}) : 
                Collections.emptyMap();
            
            return Session.builder()
                .id(rs.getString("session_id"))
                .userId(rs.getString("user_id"))
                .data(data)
                .createdAt(rs.getTimestamp("created_at").toInstant())
                .expiresAt(rs.getTimestamp("expires_at").toInstant())
                .lastAccessed(rs.getTimestamp("last_accessed").toInstant())
                .build();
        } catch (Exception e) {
            throw new DataAccessException("Failed to map session from database", e);
        }
    }
}

Transaction Configuration

@Service
@Transactional
public class SessionService {
    
    private final SessionRepository sessionRepository;
    private final BatchSessionOperations batchOperations;
    
    public SessionService(SessionRepository sessionRepository, 
                         BatchSessionOperations batchOperations) {
        this.sessionRepository = sessionRepository;
        this.batchOperations = batchOperations;
    }
    
    @Transactional(readOnly = true)
    public Optional<Session> getSession(String sessionId) {
        return sessionRepository.findById(sessionId);
    }
    
    @Transactional
    public Session createSession(Session session) {
        return sessionRepository.save(session);
    }
    
    @Transactional
    public void refreshSession(String sessionId) {
        // Update last_accessed timestamp in batch for performance
        batchOperations.updateLastAccessed(Collections.singletonList(sessionId));
    }
}

Performance Considerations

  1. Query Optimization: All queries use proper indexing and avoid SELECT *
  2. Batch Operations: Bulk operations for session cleanup and updates
  3. Connection Management: Efficient connection pooling with proper timeouts
  4. Memory Efficiency: Streaming large result sets when needed
  5. Caching Integration: Repository methods can integrate with caching layer for read-heavy operations

The repository design ensures clean separation of concerns, supports high-performance session operations, and provides flexibility for future scaling strategies like sharding or read replicas.

9. Asynchronous Processing & Messaging

Event-driven boundaries in this feature

Session management inherently involves several asynchronous operations that benefit from event-driven architecture:

  1. Session invalidation events - When a session is invalidated (logout, timeout, revocation), we need to notify connected services (e.g., websockets, other microservices) to terminate active connections or update their internal state.
  2. Session refresh events - When a session is refreshed (e.g., token renewal), we may want to update related services or audit logs.
  3. Security breach detection - If a session is flagged for suspicious activity, we need to broadcast this to prevent further unauthorized access across services.
  4. User activity tracking - Session heartbeat events can trigger analytics or monitoring systems.

These operations are handled asynchronously to avoid blocking the primary session creation/refresh flow and to ensure decoupling between services.

Message Schema Design

We'll use a JSON-based message format with versioning support:

{
  "version": "1.0",
  "event_type": "session.invalidated",
  "timestamp": "2023-10-27T14:30:00Z",
  "session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "user_id": "user_12345",
  "reason": "timeout",
  "source_service": "session-service",
  "metadata": {
    "ip_address": "192.168.1.100",
    "user_agent": "Mozilla/5.0..."
  }
}

Versioning strategy uses semantic versioning with backward compatibility in mind. Each message includes a version field to support future schema evolution.

Dead Letter Queue Strategy and Poison Message Handling

For session invalidation events, we implement a dead letter queue (DLQ) strategy:

  1. Poison Message Detection: Messages that fail to process more than 3 times are moved to DLQ
  2. DLQ Processing: Messages in DLQ are manually inspected and retried or logged for investigation
  3. Alerting: When DLQ threshold is reached (e.g., 10+ messages), alerting system triggers to notify SRE team

At-least-once vs. Exactly-once Delivery

We choose at-least-once delivery for session invalidation events because:

  1. Re-sending an invalidation event is safe - it won't cause harm if a service already processed it
  2. Session services are idempotent in handling invalidation events
  3. Simpler implementation and better performance compared to exactly-once semantics

Retry Policy for Consumers

Retry policy configuration:

  • Max attempts: 5 retries
  • Backoff strategy: Exponential with jitter (1s, 2s, 4s, 8s, 16s)
  • Non-retryable errors: Session already invalidated, user not found
  • Retry logic: Retry immediately for transient failures (network issues, DB timeouts)
public class SessionEventConsumer {
    private static final int MAX_RETRIES = 5;
    private static final long BASE_DELAY_MS = 1000L;
    
    public void processSessionEvent(SessionEvent event) {
        int attempts = 0;
        while (attempts < MAX_RETRIES) {
            try {
                handleEvent(event);
                return;
            } catch (RetryableException e) {
                attempts++;
                if (attempts >= MAX_RETRIES) {
                    moveMessageToDLQ(event);
                    break;
                }
                
                long delay = (1L << attempts) * BASE_DELAY_MS + 
                    ThreadLocalRandom.current().nextLong(0, 1000);
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Interrupted during retry", ie);
                }
            } catch (NonRetryableException e) {
                moveMessageToDLQ(event);
                break;
            }
        }
    }
}

Outbox Pattern for Reliable Event Publishing

We implement the outbox pattern to ensure session events are published reliably:

  1. Transaction-bound writes: Events are written to an outbox table within the same database transaction as session state changes
  2. Background processor: A separate worker process polls the outbox table and publishes messages to message broker
  3. Idempotency: Events are marked as processed once successfully published
public class SessionEventPublisher {
    private final OutboxRepository outboxRepository;
    private final MessageBroker messageBroker;
    
    @Transactional
    public void publishSessionEvent(SessionEvent event) {
        // Write to outbox table within same transaction
        OutboxMessage outboxMessage = new OutboxMessage(
            event.getEventType(),
            event.toJson(),
            "pending"
        );
        outboxRepository.save(outboxMessage);
        
        // Session state changes happen here
        sessionRepository.updateSessionState(event.getSessionId(), event.getNewStatus());
    }
    
    public void processOutboxMessages() {
        List<OutboxMessage> pendingMessages = outboxRepository.findPendingMessages();
        for (OutboxMessage message : pendingMessages) {
            try {
                messageBroker.publish(message.getPayload());
                message.setStatus("processed");
                outboxRepository.update(message);
            } catch (Exception e) {
                message.setStatus("failed");
                outboxRepository.update(message);
            }
        }
    }
}

Sequence Diagram for Async Flows

sequenceDiagram
    participant Client
    participant SessionService
    participant DB
    participant Outbox
    participant MessageBroker
    participant ExternalService
    
    Client->>SessionService: Invalidate session (POST /invalidate)
    SessionService->>DB: Update session status to 'invalidated'
    SessionService->>Outbox: Write event to outbox table
    SessionService-->>Client: Return success response
    
    loop Process outbox messages
        Outbox->>MessageBroker: Publish session.invalidated event
        MessageBroker->>ExternalService: Deliver to connected services
        ExternalService-->>MessageBroker: Acknowledge receipt
    end

10. Error Handling Design

Error Classification Hierarchy

The error handling strategy follows a hierarchical classification to enable appropriate response and alerting:

Error
├── DomainError (business logic violations)
│   ├── InvalidSessionError
│   ├── SessionExpiredError
│   └── InsufficientPermissionsError
├── ValidationError (input validation failures)
│   ├── InvalidTokenError
│   └── MissingRequiredFieldError
├── InfrastructureError (system-level failures)
│   ├── DatabaseConnectionError
│   ├── CacheTimeoutError
│   └── ExternalServiceError
└── InternalError (unexpected system failures)
    └── UnexpectedSystemError

Error Propagation Strategy

Errors are propagated up the call stack with appropriate wrapping to preserve context:

  • Domain and Validation errors are thrown as specific exceptions that can be caught at service boundaries for user-facing responses
  • Infrastructure errors are wrapped in retryable exceptions to enable circuit breaker patterns
  • Internal errors are logged at ERROR level and re-thrown as generic system errors to prevent information leakage

Retry Strategy with Exponential Backoff and Jitter

For retryable infrastructure errors, the following pattern is implemented:

public class RetryableException extends RuntimeException {
    private final int maxRetries;
    private final long baseDelayMs;
    private final int currentRetry;

    public RetryableException(String message, int maxRetries, long baseDelayMs, int currentRetry) {
        super(message);
        this.maxRetries = maxRetries;
        this.baseDelayMs = baseDelayMs;
        this.currentRetry = currentRetry;
    }

    public long getDelayWithJitter() {
        double exponentialDelay = baseDelayMs * Math.pow(2, currentRetry);
        double jitter = ThreadLocalRandom.current().nextDouble(0.5, 1.5);
        return (long) (exponentialDelay * jitter);
    }
}

Retry configuration:

  • Max retries: 3
  • Base delay: 100ms
  • Backoff formula: delay = baseDelay * 2^retryCount
  • Jitter: ±50% randomization to prevent thundering herd

Circuit Breaker Design

For external service dependencies (e.g., Redis, database), a circuit breaker pattern is implemented:

public class CircuitBreaker {
    private final int failureThreshold = 5;
    private final long timeoutMs = 60_000;
    private final int resetTimeoutMs = 30_000;
    
    private volatile CircuitState state = CircuitState.CLOSED;
    private int failureCount = 0;
    private long lastFailureTime = 0;
    
    public enum CircuitState {
        CLOSED, OPEN, HALF_OPEN
    }
    
    public <T> T execute(Supplier<T> operation) throws Exception {
        switch (state) {
            case CLOSED:
                try {
                    T result = operation.get();
                    onSuccess();
                    return result;
                } catch (Exception e) {
                    onFailure(e);
                    throw e;
                }
            case OPEN:
                if (System.currentTimeMillis() - lastFailureTime > resetTimeoutMs) {
                    state = CircuitState.HALF_OPEN;
                    return operation.get();
                }
                throw new CircuitBreakerOpenException("Circuit is OPEN");
            case HALF_OPEN:
                try {
                    T result = operation.get();
                    state = CircuitState.CLOSED;
                    return result;
                } catch (Exception e) {
                    state = CircuitState.OPEN;
                    throw e;
                }
        }
        throw new IllegalStateException("Invalid circuit state");
    }
    
    private void onSuccess() {
        failureCount = 0;
        state = CircuitState.CLOSED;
    }
    
    private void onFailure(Exception e) {
        failureCount++;
        lastFailureTime = System.currentTimeMillis();
        if (failureCount >= failureThreshold) {
            state = CircuitState.OPEN;
        }
    }
}

Fallback Behavior Design

Fallbacks are implemented for critical external dependencies:

public class SessionService {
    private final CircuitBreaker circuitBreaker = new CircuitBreaker();
    
    public Session getSession(String sessionId) throws SessionNotFoundException {
        try {
            return circuitBreaker.execute(() -> sessionRepository.findById(sessionId));
        } catch (Exception e) {
            // Fallback to in-memory cache or default session
            return fallbackSession(sessionId);
        }
    }
    
    private Session fallbackSession(String sessionId) {
        // Return default session or throw appropriate error
        return new Session(sessionId, "anonymous", Collections.emptyMap());
    }
}

Error Logging Format and Alerting Thresholds

Structured logging follows this schema:

{
  "timestamp": "2023-10-27T14:30:45.123Z",
  "trace_id": "abc123def456",
  "span_id": "xyz789uvw012",
  "user_id": "user-12345",
  "entity_id": "session-abc123",
  "error_code": "SESSION_EXPIRED",
  "error_message": "Session has expired",
  "error_type": "DomainError",
  "stack_trace": "...",
  "component": "session-service"
}

Alerting thresholds:

  • Critical (warn): Session service availability drops below 95% for >5 minutes
  • Warning (warn): >1000 error responses per minute from session endpoints
  • Critical (alert): >5000 error responses per minute or >99% error rate for 1 minute
  • On-call escalation: >5000 errors per minute or >99% error rate for 5 minutes

Error Response Format

Standardized JSON error response:

{
  "error": {
    "code": "SESSION_EXPIRED",
    "message": "The session has expired and is no longer valid.",
    "details": {
      "session_id": "abc123def456",
      "timestamp": "2023-10-27T14:30:45.123Z"
    }
  }
}

Error Handling in Service Layer

public class SessionService {
    public void invalidateSession(String sessionId) throws SessionNotFoundException {
        try {
            sessionRepository.delete(sessionId);
        } catch (DataAccessException e) {
            // Log and rethrow as retryable error
            logger.error("Failed to invalidate session: {}", sessionId, e);
            throw new RetryableException(
                "Database operation failed during session invalidation",
                3, 100, 1
            );
        }
    }
}

11. Security Design

Input Validation and Sanitization Strategy

All inputs are validated using a combination of:

  • Regex patterns for format validation (e.g., email, phone number)
  • Length limits to prevent buffer overflow attacks
  • Whitelisting for allowed characters and values (e.g., session ID format)
  • Custom validators for business logic constraints
public class SessionInputValidator {
    private static final Pattern SESSION_ID_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]{32}$");
    private static final Pattern USER_ID_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]{10,50}$");
    
    public static void validateSessionId(String sessionId) {
        if (sessionId == null || !SESSION_ID_PATTERN.matcher(sessionId).matches()) {
            throw new InvalidSessionIdException("Invalid session ID format");
        }
    }
    
    public static void validateUserId(String userId) {
        if (userId == null || !USER_ID_PATTERN.matcher(userId).matches()) {
            throw new InvalidUserIdException("Invalid user ID format");
        }
    }
}

SQL Injection Prevention

All database queries use parameterized statements with prepared statements to prevent SQL injection attacks:

// Instead of string concatenation
String query = "SELECT * FROM sessions WHERE session_id = '" + sessionId + "'";
// Use parameterized queries
String query = "SELECT * FROM sessions WHERE session_id = ?";
PreparedStatement stmt = connection.prepareStatement(query);
stmt.setString(1, sessionId);

Authentication Middleware Design

Authentication middleware intercepts all requests and validates:

  • JWT tokens for session authenticity
  • Session expiration timestamps
  • Session revocation status
@Component
public class SessionAuthenticationFilter implements Filter {
    private final JwtTokenProvider jwtProvider;
    private final SessionService sessionService;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String token = extractToken(httpRequest);
        
        if (token != null && jwtProvider.validateToken(token)) {
            String sessionId = jwtProvider.getSessionIdFromToken(token);
            Session session = sessionService.getActiveSession(sessionId);
            
            if (session != null && !session.isRevoked()) {
                SecurityContext context = new SessionSecurityContext(session);
                SecurityContextHolder.setContext(context);
            } else {
                throw new InvalidSessionException("Session not found or revoked");
            }
        } else {
            throw new InvalidTokenException("Invalid or expired token");
        }
        
        chain.doFilter(request, response);
    }
}

Authorization Enforcement

RBAC (Role-Based Access Control) is implemented at service layer:

  • Users have roles (admin, user, guest)
  • Session operations are restricted by role
  • Permission checks happen before operation execution
@PreAuthorize("hasRole('ADMIN') or @sessionAuthorizationService.canAccessSession(authentication, sessionId)")
public void revokeSession(String sessionId) {
    // Revoke logic here
}

@Component
public class SessionAuthorizationService {
    public boolean canAccessSession(Authentication auth, String sessionId) {
        if (auth.hasRole("ADMIN")) return true;
        
        String userId = auth.getName();
        Session session = sessionRepository.findBySessionId(sessionId);
        return session != null && session.getUserId().equals(userId);
    }
}

Sensitive Data Encryption

  • At rest: AES-256 encryption for session data in database
  • In transit: TLS 1.3+ for all API communications
@Service
public class SessionEncryptionService {
    private final Cipher cipher;
    
    public SessionEncryptionService() throws Exception {
        cipher = Cipher.getInstance("AES/GCM/NoPadding");
    }
    
    public String encryptSessionData(String data) throws Exception {
        SecretKeySpec key = new SecretKeySpec(getEncryptionKey(), "AES");
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] encrypted = cipher.doFinal(data.getBytes());
        return Base64.getEncoder().encodeToString(encrypted);
    }
}

PII Handling and Masking Strategy

  • User email is masked in logs (e.g., "j***@example.com")
  • Session tokens are truncated in logs for debugging
  • All PII is encrypted at rest and never logged in plain text

Secrets Management

  • Session signing keys stored in AWS Secrets Manager (rotated every 90 days)
  • Database credentials managed through Vault with automatic rotation
  • All secrets accessed through secure channels only

Audit Logging Design

All session operations are logged with:

  • Timestamp and user ID
  • Session ID and operation type
  • IP address and user agent
  • Success/failure status
{
  "timestamp": "2023-12-01T10:30:45Z",
  "userId": "user_1234567890",
  "sessionId": "session_abc123def456",
  "operation": "SESSION_CREATE",
  "ipAddress": "192.168.1.100",
  "userAgent": "Mozilla/5.0...",
  "status": "SUCCESS"
}

12. Testing Strategy

Unit Testing Scope and Coverage Targets (%)

  • Service Layer: 85% code coverage with focus on business logic, edge cases, and error paths
  • Repository Layer: 90% coverage with all CRUD operations and query edge cases
  • Domain Objects: 100% coverage for value objects, DTOs, and validation logic
  • Security Components: 95% coverage for authentication/authorization logic and input sanitization
  • Critical Path Operations: 100% coverage for all methods in the session lifecycle (create, validate, refresh, invalidate)
  • Test Data: All test cases should use factory patterns and fixtures to ensure deterministic behavior

Integration Testing Strategy

  • Database Integration: Use Testcontainers with PostgreSQL 13+ to test actual database interactions, including transaction boundaries and concurrent access patterns
  • External Service Mocks: Use WireMock or Mockito for external dependencies like Redis cache and JWT providers
  • Authentication Integration: Test end-to-end flows with mocked OAuth2 provider to validate token handling and session creation
  • Caching Layer: Validate cache behavior with embedded Redis instance to ensure proper TTL handling and eviction
  • Messaging Integration: Use embedded Kafka or RabbitMQ for async flow testing, particularly for session invalidation events

Contract Testing

  • API Contracts: Use Pact to define and test contracts between session management service and consumer services (e.g., auth server, frontend)
  • Event Contracts: Define Avro schemas for session events and use Pact to verify that producers and consumers agree on message format
  • Versioned Endpoints: Test API version compatibility with backward-compatibility checks for session token format evolution

Performance Testing Tool and Key Scenarios

  • Tool: k6 (JavaScript-based load testing) for high-concurrency session management tests
  • Key Scenarios:
    • Concurrent session creation and validation (10k concurrent users)
    • Session refresh rate testing under load (5000 sessions/sec)
    • Cache hit ratio validation (target: >95% for session lookups)
    • Memory usage under sustained load (monitor GC pressure and heap allocation)
    • Latency distribution testing (p50 < 5ms, p95 < 20ms, p99 < 100ms)
  • Test Metrics: Throughput, response time percentiles, error rates, and resource utilization (CPU, memory, DB connections)

Test Data Management Strategy

  • Fixtures: Use JSON/YAML fixtures for session state templates (active, expired, invalidated)
  • Factories: Implement factory classes to generate realistic session data with varying TTLs and user roles
  • Seeding: Use database migration scripts to seed test data for integration tests, including edge cases (session with expired refresh tokens)
  • Isolation: Each test should run in isolated database transaction or clean state to prevent cross-test contamination
  • Data Generation: Use libraries like Faker for generating realistic user IDs, session tokens, and timestamps
  • Cleanup: Implement automatic cleanup of test data with database transaction rollback or deletion hooks after each test suite run
// Example unit test for session creation with proper isolation and coverage
@Test
public void testCreateSessionWithValidUser() {
    // Given
    User user = UserFactory.createValidUser();
    SessionRequest request = new SessionRequest(user.getId(), "192.168.1.1");
    
    // When
    Session session = sessionService.createSession(request);
    
    // Then
    assertThat(session).isNotNull();
    assertThat(session.getId()).isNotBlank();
    assertThat(session.getUserId()).isEqualTo(user.getId());
    assertThat(session.getCreatedAt()).isNotNull();
    assertThat(session.getExpiresAt()).isAfter(session.getCreatedAt());
}

13. Observability Design

Structured Logging Schema

The logging system follows a structured JSON format with consistent fields to enable efficient querying and analysis:

{
  "timestamp": "2023-10-15T14:30:45.123Z",
  "level": "INFO",
  "service": "session-management-service",
  "trace_id": "1234567890abcdef1234567890abcdef",
  "span_id": "abcdef1234567890abcdef1234567890",
  "user_id": "user-12345",
  "session_id": "sess-98765",
  "request_id": "req-1234567890",
  "method": "POST",
  "endpoint": "/api/v1/sessions",
  "duration_ms": 45,
  "status_code": 201,
  "error_code": null,
  "error_message": null,
  "event_type": "session_created",
  "ip_address": "192.168.1.100",
  "user_agent": "Mozilla/5.0...",
  "request_body": "{...}",
  "response_body": "{...}"
}

PII masking rules:

  • User ID: Masked in logs (showing only first 4 characters)
  • IP Address: Partially masked (last octet replaced with xxx)
  • User-Agent: Truncated to first 100 characters
  • Request/Response bodies: Only first 500 characters logged for sensitive fields

Distributed Tracing Instrumentation

OpenTelemetry is used for distributed tracing with custom spans:

Key Operations to Trace:

  1. session.create - Create new session
  2. session.validate - Validate existing session
  3. session.refresh - Refresh session TTL
  4. session.invalidate - Invalidate session

Span Attributes:

// Example span attributes for session creation
span.setAttribute("session.id", sessionId);
span.setAttribute("session.user_id", userId);
span.setAttribute("session.ip_address", clientIp);
span.setAttribute("session.device_type", deviceType);
span.setAttribute("session.os", os);
span.setAttribute("session.browser", browser);

Custom Spans:

  • session.create.start → session.create.end
  • session.validate.start → session.validate.end
  • cache.hit / cache.miss for session lookup operations

Metrics Design

RED Method Metrics (Rate, Errors, Duration):

Metric NameTypeLabelsDescription
session_requests_totalCountermethod, endpoint, status_codeTotal session API requests
session_requests_errors_totalCountermethod, endpoint, error_typeSession request errors
session_requests_duration_secondsHistogrammethod, endpointRequest processing time
session_cache_hits_totalCountercache_type, operationCache hit statistics
session_cache_misses_totalCountercache_type, operationCache miss statistics
session_store_operations_totalCounteroperation, statusDatabase store operations
session_active_countGaugeenvironmentActive sessions count

Label Design:

  • method: GET, POST, PUT, DELETE
  • endpoint: /api/v1/sessions, /api/v1/sessions/{id}
  • status_code: 200, 400, 401, 404, 500
  • error_type: validation_error, database_error, cache_error
  • operation: create, validate, refresh, invalidate

Health Check Endpoint Design

/health: 
  - Checks database connectivity
  - Verifies cache service availability  
  - Validates external dependencies (auth service)
  - Returns status: "healthy" or "unhealthy"
  - Response format:
    {
      "status": "healthy",
      "timestamp": "2023-10-15T14:30:45Z",
      "checks": {
        "database": {"status": "healthy", "latency_ms": 12},
        "cache": {"status": "healthy", "latency_ms": 5},
        "auth_service": {"status": "healthy", "latency_ms": 25}
      }
    }

/ready:
  - Returns "ready" when service can accept traffic
  - Checks that all required dependencies are available
  - Returns "not ready" if dependencies are failing

/live:
  - Basic liveness check
  - Returns "alive" if service is running
  - Does not check dependencies, only process health

/startup:
  - Returns detailed startup status
  - Shows initialization progress of components

Alerting Thresholds

MetricWarning LevelCritical LevelEscalation Path
session_requests_errors_total> 5 errors/minute> 20 errors/minuteOn-call engineer
session_requests_duration_secondsP95 > 100msP95 > 500msSRE team
session_cache_misses_total> 1000 misses/minute> 5000 misses/minuteBackend team
session_store_operations_total> 1000 operations/minute> 5000 operations/minuteDatabase team
session_active_count< 100 active sessions< 10 active sessionsOperations team
database_connection_errors> 5 errors/minute> 20 errors/minuteDBA team

Alerting Logic:

  • Use sliding window of 5-minute intervals
  • Apply exponential backoff for repeated alerts
  • Implement alert deduplication to prevent noise
  • Include SLO violation thresholds (e.g., 95% success rate)

14. Open Questions, Risks & Trade-offs

Decisions requiring product / engineering alignment

  • Session storage strategy: Should sessions be stored in-memory (fast but not scalable) or distributed (Redis, database) - requires infrastructure and scale commitments from the platform team
  • Session invalidation policy: Should sessions be invalidated on logout, or only after TTL expires - impacts UX vs. security trade-offs
  • Cross-service session sharing: Whether to support single sign-on (SSO) across microservices or maintain isolated sessions per service - affects system architecture complexity
  • Mobile vs. web session handling: Different session lifecycles and refresh strategies for mobile apps vs. web browsers - requires separate handling logic
  • Session migration strategy: How to handle session migration when service instances change - impacts session continuity and user experience

Known trade-offs and justifications

Trade-offOption 1Option 2Justification
Storage Performance vs. ScalabilityIn-memory storage (fast reads/writes)Distributed cache/databaseIn-memory is faster but doesn't scale across service instances. Distributed storage enables horizontal scaling but adds network latency and complexity.
Session Expiry StrategyImmediate invalidation on logoutTTL-based expirationImmediate invalidation provides better security but requires additional coordination. TTL-based is more scalable but introduces potential security gaps if sessions are not properly invalidated.
Session Refresh StrategyManual refresh on every requestAutomatic refresh with sliding windowManual refresh provides fine-grained control but increases client-side complexity. Automatic refresh improves UX but may increase server load and session maintenance overhead.
Concurrency ControlOptimistic locking (version-based)Pessimistic locking (exclusive locks)Optimistic locking provides better performance and avoids deadlocks but requires conflict handling. Pessimistic locking prevents race conditions but introduces lock contention and potential deadlocks.
Security vs. PerformanceJWT-based sessions (stateless)Database-backed sessions (stateful)JWT sessions eliminate database lookups but increase payload size and require token revocation mechanisms. Database-backed sessions provide better security control but introduce database dependencies.

Risks and mitigation strategies

Risk: Session hijacking attacks

  • Mitigation: Implement secure, HttpOnly, SameSite cookies with proper session ID generation and rotation. Use HTTPS-only sessions and implement IP address binding where appropriate.

Risk: Session replay attacks

  • Mitigation: Implement session ID uniqueness constraints, timestamp validation, and session token rotation. Use short-lived sessions with automatic refresh mechanisms.

Risk: Scalability limitations with in-memory session storage

  • Mitigation: Design the system to support distributed session storage from the start. Implement proper session replication or sharding strategies.

Risk: Session state inconsistency across service instances

  • Mitigation: Use distributed cache with consistent replication or database-backed sessions. Implement proper session synchronization mechanisms.

Risk: Performance degradation from frequent session lookups

  • Mitigation: Implement caching layer with appropriate TTLs and cache warming strategies. Use read replicas for session data access.

Risk: Memory leaks in session storage

  • Mitigation: Implement proper session cleanup mechanisms with TTL-based eviction and regular garbage collection processes.

Technical debt acknowledged

  • Session state management complexity: The system introduces additional complexity in managing session lifecycle, especially with cross-service session handling and synchronization
  • Token size overhead: JWT-based sessions increase payload size, which can impact network performance and bandwidth usage
  • Session invalidation complexity: Implementing proper session invalidation across distributed systems requires careful design and can introduce race conditions
  • Cross-service session consistency: Maintaining session consistency across microservices introduces additional coordination overhead and potential failure points
  • Caching strategy maintenance: The caching layer requires ongoing tuning and monitoring to ensure optimal performance and data consistency
  • Security implementation overhead: Comprehensive session security measures require ongoing maintenance and updates to address evolving threats

Was this article helpful?

Topics in this article

More from Backend LLD

  • Frontend LLD

    Automatic Token Refresh System Design

    Comprehensive low-level design for implementing automatic JWT token refresh in frontend applications with error handling and concurrency control

  • Frontend LLD

    JWT Token Management System Design

    Comprehensive low-level design for secure JWT token handling, refresh mechanisms, and session management in modern web applications.

  • Frontend LLD

    Client-Side Authentication Manager Design

    Comprehensive low-level design for a secure, scalable client-side authentication system with token management and session handling.

  • Backend LLD

    API Key Management System Design

    Comprehensive low-level design for a scalable API key management system with rotation, revocation, and usage tracking capabilities.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.