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
- What scale expectations are we targeting? What's the expected number of concurrent active sessions, peak requests per second, and total session count?
- What consistency requirements exist? Should session invalidation be immediate across all services (strong consistency) or eventual consistency acceptable?
- What are the key entities and their relationships? Is session tied to user, device, or application? How does session expiration interact with refresh tokens?
- What's the read/write ratio for sessions? Are we primarily reading session data or frequently creating/invalidating sessions?
- What are the latency sensitivity requirements? What's the acceptable P95/P99 response time for session validation operations?
- What authentication model is in use? Is it JWT-based, OAuth2, or session-cookie based? Are there external identity providers involved?
- 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
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| CreateSession | /v1/session/create | Creates a new user session | None | 100 req/min |
| GetSession | /v1/session/get | Retrieves session by ID | JWT | 1000 req/min |
| RefreshSession | /v1/session/refresh | Extends session validity | JWT | 50 req/min |
| InvalidateSession | /v1/session/invalidate | Invalidates a session | JWT | 50 req/min |
| InvalidateAllSessions | /v1/session/invalidate/all | Invalidates all user sessions | JWT | 10 req/min |
| ListActiveSessions | /v1/session/list | Lists all active sessions for user | JWT | 20 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 Code | Description |
|---|---|
| 200 | Success |
| 400 | Bad Request - Invalid input |
| 401 | Unauthorized - Invalid or missing auth |
| 403 | Forbidden - Insufficient permissions |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Internal 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-Keyheader 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
-
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 checksidx_sessions_last_accessed_at: For session activity tracking and rotation
-
Users Table:
idx_users_email: For email-based authentication lookups
-
User Devices Table:
idx_user_devices_user_id: For device-based session managementidx_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 creationupdated_at: Timestamp of last modification (for users table)last_accessed_at: For sessions, tracks last activity for rotation logiccreated_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 = FALSEare 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:
- Session creation and persistence
- Token generation (with proper security)
- Event publishing (ensured atomic with session save)
- 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:
- Atomic operations for session counter increments and decrement operations
- Lock-based synchronization for critical session state updates (token refresh, session invalidation)
- Compare-and-swap (CAS) operations for optimistic updates to session metadata
- 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:
- Lock ordering - All session operations acquire locks in consistent order (by session ID hash)
- Timeout-based lock acquisition - 50ms timeout for lock acquisition to prevent indefinite waits
- Hierarchical lock management - Session-level locks are acquired before global cache locks
- No circular dependency patterns in session state updates
Idempotency Implementation for Concurrent Duplicate Requests
Idempotency is handled at multiple levels:
- Request deduplication using request ID and timestamp in a distributed cache
- Token-based idempotency keys for session refresh operations
- 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:
- ConcurrentHashMap for in-memory session storage with atomic operations
- Read-write locks to allow concurrent reads while protecting writes
- Immutable session objects to prevent modification during read operations
- 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 Type | TTL | Reason |
|---|---|---|
| Active Sessions | 30 minutes | Session timeout after inactivity |
| User Sessions | 24 hours | Keep user session for re-authentication |
| Session Tokens | 1 hour | Token expiration for security |
| IP-based sessions | 5 minutes | Short-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:
- Probabilistic early expiry: Set cache TTL to 80% of actual expiration time
- Distributed locks: Use Redis distributed locks for cache population
- 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-lruwithmaxmemoryset to 2GB per node
Cache Invalidation Strategy
We'll implement event-driven invalidation:
- Session expiration → Immediate cache removal
- Session logout → Cache invalidation via event
- User session update → Cache refresh with new data
- 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
- Query Optimization: All queries use proper indexing and avoid SELECT *
- Batch Operations: Bulk operations for session cleanup and updates
- Connection Management: Efficient connection pooling with proper timeouts
- Memory Efficiency: Streaming large result sets when needed
- 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:
- 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.
- Session refresh events - When a session is refreshed (e.g., token renewal), we may want to update related services or audit logs.
- Security breach detection - If a session is flagged for suspicious activity, we need to broadcast this to prevent further unauthorized access across services.
- 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:
- Poison Message Detection: Messages that fail to process more than 3 times are moved to DLQ
- DLQ Processing: Messages in DLQ are manually inspected and retried or logged for investigation
- 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:
- Re-sending an invalidation event is safe - it won't cause harm if a service already processed it
- Session services are idempotent in handling invalidation events
- 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:
- Transaction-bound writes: Events are written to an outbox table within the same database transaction as session state changes
- Background processor: A separate worker process polls the outbox table and publishes messages to message broker
- 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:
session.create- Create new sessionsession.validate- Validate existing sessionsession.refresh- Refresh session TTLsession.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.endsession.validate.start→session.validate.endcache.hit/cache.missfor session lookup operations
Metrics Design
RED Method Metrics (Rate, Errors, Duration):
| Metric Name | Type | Labels | Description |
|---|---|---|---|
session_requests_total | Counter | method, endpoint, status_code | Total session API requests |
session_requests_errors_total | Counter | method, endpoint, error_type | Session request errors |
session_requests_duration_seconds | Histogram | method, endpoint | Request processing time |
session_cache_hits_total | Counter | cache_type, operation | Cache hit statistics |
session_cache_misses_total | Counter | cache_type, operation | Cache miss statistics |
session_store_operations_total | Counter | operation, status | Database store operations |
session_active_count | Gauge | environment | Active sessions count |
Label Design:
method: GET, POST, PUT, DELETEendpoint:/api/v1/sessions,/api/v1/sessions/{id}status_code: 200, 400, 401, 404, 500error_type: validation_error, database_error, cache_erroroperation: 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
| Metric | Warning Level | Critical Level | Escalation Path |
|---|---|---|---|
session_requests_errors_total | > 5 errors/minute | > 20 errors/minute | On-call engineer |
session_requests_duration_seconds | P95 > 100ms | P95 > 500ms | SRE team |
session_cache_misses_total | > 1000 misses/minute | > 5000 misses/minute | Backend team |
session_store_operations_total | > 1000 operations/minute | > 5000 operations/minute | Database team |
session_active_count | < 100 active sessions | < 10 active sessions | Operations team |
database_connection_errors | > 5 errors/minute | > 20 errors/minute | DBA 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-off | Option 1 | Option 2 | Justification |
|---|---|---|---|
| Storage Performance vs. Scalability | In-memory storage (fast reads/writes) | Distributed cache/database | In-memory is faster but doesn't scale across service instances. Distributed storage enables horizontal scaling but adds network latency and complexity. |
| Session Expiry Strategy | Immediate invalidation on logout | TTL-based expiration | Immediate 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 Strategy | Manual refresh on every request | Automatic refresh with sliding window | Manual refresh provides fine-grained control but increases client-side complexity. Automatic refresh improves UX but may increase server load and session maintenance overhead. |
| Concurrency Control | Optimistic 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. Performance | JWT-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