Skip to main content

OAuth Service Design and Implementation Guide

Comprehensive low-level design document for building a scalable OAuth 2.0 service with token management, client authentication, and security best practices.

This document presents a production-ready low-level design for implementing an OAuth 2.0 service that handles token lifecycle management, client validation, and secure authorization flows for modern distributed systems.

1. Problem Statement & Requirements

Clarifying questions

  1. What scale and traffic expectations are we targeting?
    • Is this a service for millions of users with high concurrent request volume (e.g., 100k+ RPS) or a smaller-scale internal auth system?
  2. What consistency and availability SLAs are required?
    • Should the OAuth service support sub-100ms response times (p99 < 200ms) with 99.9% availability?
  3. What are the key entities and their relationships?
    • Are we designing for a single tenant or multi-tenant model? What are the relationships between users, clients, tokens, scopes?
  4. What's the read/write ratio for token and user operations?
    • Is it primarily token issuance (write-heavy) or frequent token validation (read-heavy)?
  5. How sensitive is the system to latency?
    • Are there strict requirements for token validation to be sub-50ms in all cases?
  6. What authentication model does the system support?
    • Should it support only OAuth2 flows (Authorization Code, Implicit, Client Credentials) or also OpenID Connect?
  7. Are there external integrations with identity providers (IdPs) or SSO systems?
    • Will the service be federating with Okta, Auth0, or other IdPs?

Functional requirements

  • Support OAuth2 flows (Authorization Code, Implicit, Client Credentials, Refresh Token)
  • Issue and validate JWT access tokens and refresh tokens
  • Manage OAuth2 clients (registration, configuration, scopes)
  • Support OpenID Connect (OIDC) for identity assertions
  • Allow token revocation and refresh
  • Support PKCE (Proof Key for Code Exchange) for public clients
  • Provide token introspection endpoint to validate tokens
  • Support scope-based access control (RBAC-style)
  • Allow client and user consent management

Non-functional requirements

RequirementTarget
p50 latency (token issuance)< 50ms
p95 latency (token issuance)< 100ms
p99 latency (token issuance)< 200ms
Throughput100k+ RPS (peak)
Availability99.9% (99.99% for critical flows)
Token validation latency< 50ms (p99)
Concurrent token issuanceSupport up to 10k concurrent users

Constraints and assumptions

  • The system is designed for a multi-tenant SaaS environment
  • Authentication is handled via an external identity provider (e.g., Okta or Azure AD) or internal user store
  • Token validation must be fast and stateless where possible (e.g., JWT-based)
  • The system assumes a centralized database for storing client, user, and token metadata
  • All tokens are expected to have TTLs (e.g., 1 hour for access, 30 days for refresh)
  • Token rotation is not required in this design (but can be added later)

Out of scope

  • User registration or password reset flows
  • Multi-factor authentication (MFA) support
  • Token encryption at rest (only JWTs with signing are supported)
  • OAuth2 delegation or proxy flows (e.g., SAML to OAuth2 bridging)
  • Token revocation via Webhooks or push notifications
  • Integration with external identity providers (e.g., SAML, LDAP) beyond basic user store access
  • Support for OAuth2 device flow or resource owner password credentials (ROPC) flows

2. API Design

API Style Selection

gRPC with Protobuf is selected for the OAuth service due to:

  • Strong typing and schema evolution support
  • Efficient binary serialization reducing payload size
  • Built-in support for streaming and bidirectional communication
  • Native support for authentication and authorization headers
  • Better performance characteristics for microservices communication

Endpoint Definitions

MethodPathDescriptionAuth
POST/oauth/v1/tokenExchange authorization code for access tokenNone
POST/oauth/v1/refreshRefresh access token using refresh tokenNone
GET/oauth/v1/authorizeAuthorization code request endpointUser Auth
POST/oauth/v1/revokeRevoke access/refresh tokenUser Auth
GET/oauth/v1/userinfoGet authenticated user infoBearer Token
POST/oauth/v1/clientRegister new OAuth clientAdmin Auth
PUT/oauth/v1/client/{id}Update existing OAuth clientAdmin Auth
DELETE/oauth/v1/client/{id}Delete OAuth clientAdmin Auth

Request/Response Schemas

Token Request (gRPC)

message TokenRequest {
  string grant_type = 1;
  string client_id = 2;
  string client_secret = 3;
  string code = 4;
  string redirect_uri = 5;
  string refresh_token = 6;
}

message TokenResponse {
  string access_token = 1;
  string token_type = 2;
  int32 expires_in = 3;
  string refresh_token = 4;
  string scope = 5;
}

Client Registration Request (gRPC)

message ClientRequest {
  string client_name = 1;
  string client_uri = 2;
  repeated string redirect_uris = 3;
  string logo_uri = 4;
  string client_type = 5; // "public" or "confidential"
  repeated string grant_types = 6;
  repeated string response_types = 7;
  string token_endpoint_auth_method = 8; // "client_secret_basic", "client_secret_post", "none"
}

User Info Response (gRPC)

message UserInfoResponse {
  string sub = 1; // Subject identifier
  string name = 2;
  string email = 3;
  string preferred_username = 4;
  google.protobuf.Timestamp updated_at = 5;
}

HTTP Status Codes & Error Response

Standard HTTP status codes:

  • 200 OK - Successful response
  • 201 Created - Resource created
  • 400 Bad Request - Invalid request parameters
  • 401 Unauthorized - Authentication required or invalid credentials
  • 403 Forbidden - Insufficient permissions
  • 404 Not Found - Resource not found
  • 429 Too Many Requests - Rate limiting exceeded

Standardized error response format:

{
  "error": {
    "code": "INVALID_GRANT",
    "message": "The authorization code is invalid or expired",
    "details": {
      "field": "code",
      "value": "expired_code"
    }
  }
}

API Versioning Strategy

Versioned using path prefix: /oauth/v1/... with support for:

  • Backward compatibility within same major version
  • Migration path documented in changelog
  • Deprecation policy with 6-month notice period

Pagination Design

Cursor-based pagination selected because:

  • More efficient for large datasets
  • Consistent results even with concurrent writes
  • Simpler to implement and maintain

Authentication & Authorization

Authentication:

  • OAuth 2.0 Bearer Token for all protected endpoints
  • Client credentials for client registration and management

Authorization:

  • JWT-based claims verification for user scopes
  • Admin role required for client management endpoints
  • Scope validation per endpoint (e.g., read:user for /userinfo)

Rate Limiting

Per Endpoint Limits:

EndpointLimitPeriod
/oauth/v1/token1000 req/min1 minute
/oauth/v1/refresh500 req/min1 minute
/oauth/v1/revoke200 req/min1 minute
/oauth/v1/userinfo5000 req/min1 minute
/oauth/v1/client100 req/min1 minute

Enforcement Strategy:

  • Token bucket algorithm with leaky bucket refinement
  • Distributed rate limiting using Redis with atomic operations

Idempotency Design

Idempotency Key Strategy:

  • X-Idempotency-Key header for all mutation endpoints
  • Store in Redis with TTL of 5 minutes
  • Return X-Idempotency-Result header indicating if operation was replayed
// Example usage in service layer
public TokenResponse handleTokenRequest(TokenRequest request, String idempotencyKey) {
    if (idempotencyKey != null) {
        String cachedResult = redisTemplate.get(idempotencyKey);
        if (cachedResult != null) {
            return parseCachedResponse(cachedResult);
        }
    }

    // Process request...
    TokenResponse response = processRequest(request);

    if (idempotencyKey != null) {
        redisTemplate.setex(idempotencyKey, 300, serializeResponse(response));
    }

    return response;
}

3. Data Model Design

Entity-Relationship Diagram

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

    CLIENT {
        string id PK
        string secret
        string name
        string redirect_uri
        string logo_url
        boolean is_active
        timestamp created_at
        timestamp updated_at
    }

    AUTHORIZATION_CODE {
        string id PK
        string user_id FK
        string client_id FK
        string scope
        timestamp expires_at
        timestamp created_at
        boolean is_used
    }

    ACCESS_TOKEN {
        string id PK
        string user_id FK
        string client_id FK
        string scope
        timestamp expires_at
        timestamp created_at
        boolean is_revoked
    }

    REFRESH_TOKEN {
        string id PK
        string user_id FK
        string client_id FK
        timestamp expires_at
        timestamp created_at
        boolean is_revoked
    }

    OAUTH_SESSION {
        string id PK
        string user_id FK
        string client_id FK
        string state
        string nonce
        string redirect_uri
        timestamp created_at
        timestamp expires_at
    }

    USER_CONSENT {
        string id PK
        string user_id FK
        string client_id FK
        string scope
        timestamp granted_at
    }

    USER <-o-- OAUTH_SESSION : "owns"
    CLIENT <-o-- OAUTH_SESSION : "owns"
    USER <-o-- AUTHORIZATION_CODE : "owns"
    CLIENT <-o-- AUTHORIZATION_CODE : "owns"
    USER <-o-- ACCESS_TOKEN : "owns"
    CLIENT <-o-- ACCESS_TOKEN : "owns"
    USER <-o-- REFRESH_TOKEN : "owns"
    CLIENT <-o-- REFRESH_TOKEN : "owns"
    USER <-o-- USER_CONSENT : "owns"
    CLIENT <-o-- USER_CONSENT : "owns"

Table 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_email_verified BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    last_login_at TIMESTAMP WITH TIME ZONE,
    
    INDEX idx_users_email (email),
    INDEX idx_users_created_at (created_at)
);

OAuth Clients Table

CREATE TABLE oauth_clients (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    secret TEXT NOT NULL,
    redirect_uri TEXT NOT NULL,
    logo_url TEXT,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_oauth_clients_active (is_active),
    INDEX idx_oauth_clients_created_at (created_at)
);

OAuth Sessions Table

CREATE TABLE oauth_sessions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID NOT NULL REFERENCES oauth_clients(id) ON DELETE CASCADE,
    state VARCHAR(255) NOT NULL,
    nonce VARCHAR(255),
    redirect_uri TEXT NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    
    INDEX idx_oauth_sessions_user_client (user_id, client_id),
    INDEX idx_oauth_sessions_expires_at (expires_at),
    INDEX idx_oauth_sessions_created_at (created_at)
);

Authorization Codes Table

CREATE TABLE authorization_codes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID NOT NULL REFERENCES oauth_clients(id) ON DELETE CASCADE,
    scope TEXT NOT NULL,
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    is_used BOOLEAN DEFAULT FALSE,
    
    INDEX idx_auth_codes_user_client (user_id, client_id),
    INDEX idx_auth_codes_expires_at (expires_at),
    INDEX idx_auth_codes_used (is_used)
);

Access Tokens Table

CREATE TABLE access_tokens (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID NOT NULL REFERENCES oauth_clients(id) ON DELETE CASCADE,
    scope TEXT NOT NULL,
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    is_revoked BOOLEAN DEFAULT FALSE,
    
    INDEX idx_access_tokens_user_client (user_id, client_id),
    INDEX idx_access_tokens_expires_at (expires_at),
    INDEX idx_access_tokens_revoked (is_revoked)
);

Refresh Tokens Table

CREATE TABLE refresh_tokens (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID NOT NULL REFERENCES oauth_clients(id) ON DELETE CASCADE,
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    is_revoked BOOLEAN DEFAULT FALSE,
    
    INDEX idx_refresh_tokens_user_client (user_id, client_id),
    INDEX idx_refresh_tokens_expires_at (expires_at),
    INDEX idx_refresh_tokens_revoked (is_revoked)
);

User Consents Table

CREATE TABLE user_consents (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID NOT NULL REFERENCES oauth_clients(id) ON DELETE CASCADE,
    scope TEXT NOT NULL,
    granted_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_user_consents_user_client (user_id, client_id),
    INDEX idx_user_consents_granted_at (granted_at)
);

Primary Key Strategy

UUIDs are used as primary keys for all tables. This choice provides:

  1. Scalability: No need to coordinate across shards or databases
  2. Security: No sequential predictability that could be exploited
  3. Distributed systems: Easier to merge data from different sources without ID conflicts
  4. No hotspots: Avoids write contention in clustered environments

Index Design

TableIndex NameFieldsPurpose
usersidx_users_emailemailFast email lookups
usersidx_users_created_atcreated_atAudit and reporting
oauth_clientsidx_oauth_clients_activeis_activeFast client filtering
oauth_sessionsidx_oauth_sessions_user_clientuser_id, client_idSession lookup by user/client
oauth_sessionsidx_oauth_sessions_expires_atexpires_atCleanup of expired sessions
authorization_codesidx_auth_codes_user_clientuser_id, client_idFast code lookup
authorization_codesidx_auth_codes_expires_atexpires_atCleanup of expired codes
access_tokensidx_access_tokens_user_clientuser_id, client_idFast token lookup
access_tokensidx_access_tokens_expires_atexpires_atCleanup of expired tokens
refresh_tokensidx_refresh_tokens_user_clientuser_id, client_idFast refresh token lookup
refresh_tokensidx_refresh_tokens_expires_atexpires_atCleanup of expired tokens
user_consentsidx_user_consents_user_clientuser_id, client_idFast consent lookup

Data Partitioning / Sharding Strategy

Shard by User ID: OAuth data is naturally user-centric, so partitioning by user_id provides:

  1. User isolation: All OAuth data for a user stays together
  2. Query locality: User-specific operations are fast and localized
  3. Scalability: Easy to scale per user volume

Soft Delete vs. Hard Delete Decision

Soft delete is used for all tables to support:

  1. Audit trails: Historical tracking of user consent and token usage
  2. Compliance: GDPR compliance for data retention requirements
  3. Recovery capability: Ability to restore deleted records if needed
  4. Analytics: Historical analysis of user behavior patterns

Audit Trail Design

All tables have created_at and updated_at timestamps. Additionally:

  1. User activity logs: Track login times, consent grants, and token creation
  2. Admin actions: Log when clients are activated/deactivated or scopes changed
  3. Event-based audit: Use a separate audit_events table for detailed tracking

Data Archival Strategy and Retention Policy

  1. Access tokens: 30 days retention (after revocation)
  2. Refresh tokens: 90 days retention (after revocation)
  3. Authorization codes: 15 minutes retention (after use or expiration)
  4. User consents: Indefinite retention (user can revoke at any time)
  5. OAuth sessions: 1 hour retention (after completion or expiration)

Archival process:

  • Move expired tokens to archive tables
  • Log archival events for compliance
  • Purge archived data after 90 days

4. Class Design & Service Layer

classDiagram
    class OAuthService {
        +authenticateUser(username, password)
        +generateToken(user)
        +validateToken(token)
        +revokeToken(token)
        +refreshToken(refreshToken)
    }
    
    class TokenService {
        +generateAccessToken(user, scope)
        +generateRefreshToken(user)
        +validateAccessToken(token)
        +validateRefreshToken(token)
        +revokeToken(token)
    }
    
    class UserService {
        +getUserById(id)
        +getUserByUsername(username)
        +validateCredentials(username, password)
    }
    
    class TokenRepository {
        +saveToken(token)
        +findTokenByValue(value)
        +revokeToken(token)
        +findTokensByUserId(userId)
    }
    
    class AuthCodeService {
        +generateAuthCode(user, clientId, scope)
        +validateAuthCode(code)
        +consumeAuthCode(code)
    }
    
    class AuthCodeRepository {
        +saveAuthCode(authCode)
        +findAuthCodeByValue(value)
        +deleteAuthCode(code)
    }
    
    class ClientService {
        +getClientById(id)
        +validateClientSecret(clientId, secret)
    }
    
    class EventPublisher {
        +publishTokenGeneratedEvent(token)
        +publishTokenRevokedEvent(token)
    }
    
    OAuthService --> TokenService : uses
    OAuthService --> UserService : uses
    OAuthService --> AuthCodeService : uses
    OAuthService --> ClientService : uses
    TokenService --> TokenRepository : persists
    AuthCodeService --> AuthCodeRepository : persists
    OAuthService --> EventPublisher : publishes
    
    class Token {
        -id: UUID
        -value: String
        -type: TokenType
        -userId: UUID
        -clientId: UUID
        -expiresAt: Instant
        -createdAt: Instant
        -revokedAt: Instant
    }
    
    class AuthCode {
        -id: UUID
        -value: String
        -userId: UUID
        -clientId: UUID
        -scope: String
        -expiresAt: Instant
        -createdAt: Instant
        -consumed: boolean
    }
    
    class User {
        -id: UUID
        -username: String
        -passwordHash: String
        -email: String
        -isActive: boolean
    }
    
    class Client {
        -id: UUID
        -clientId: String
        -clientSecret: String
        -redirectUri: String
        -name: String
        -isActive: boolean
    }
    
    class TokenType {
        ACCESS_TOKEN
        REFRESH_TOKEN
    }
stateDiagram-v2
    [*] --> Active
    Active --> Revoked : Token Revoked
    Active --> Expired : Token Expires
    Revoked --> [*]
    Expired --> [*]

Service Class Structure and Responsibilities

OAuthService: Main entry point for OAuth operations. Handles authentication flow, token generation, validation, and revocation.

TokenService: Manages token lifecycle including generation, validation, and revocation logic.

UserService: Handles user authentication and identity verification.

AuthCodeService: Manages authorization code flow operations.

ClientService: Validates client credentials and manages OAuth clients.

Interface / Abstract Class Design

public interface TokenGenerator {
    Token generateToken(User user, String scope);
}

public interface TokenValidator {
    boolean validateToken(String token);
    Token getValidToken(String token);
}

public interface TokenRepository {
    void save(Token token);
    Token findByValue(String value);
    void revoke(Token token);
    List<Token> findByUserId(UUID userId);
}

public interface AuthCodeRepository {
    void save(AuthCode authCode);
    AuthCode findByValue(String value);
    void delete(String code);
}

Business Logic Encapsulation Patterns

Strategy Pattern: Token generation strategies for different grant types.

public enum GrantType {
    PASSWORD,
    CLIENT_CREDENTIALS,
    AUTHORIZATION_CODE,
    REFRESH_TOKEN
}

public interface TokenGenerationStrategy {
    Token generateToken(OAuthContext context);
}

@Component
public class PasswordGrantStrategy implements TokenGenerationStrategy {
    private final UserService userService;
    private final TokenService tokenService;
    
    @Override
    public Token generateToken(OAuthContext context) {
        User user = userService.authenticate(
            context.getUsername(), 
            context.getPassword()
        );
        
        return tokenService.generateAccessToken(user, context.getScope());
    }
}

@Component
public class AuthorizationCodeGrantStrategy implements TokenGenerationStrategy {
    private final AuthCodeService authCodeService;
    private final TokenService tokenService;
    
    @Override
    public Token generateToken(OAuthContext context) {
        AuthCode authCode = authCodeService.validateAndConsume(
            context.getAuthorizationCode()
        );
        
        return tokenService.generateAccessToken(
            authCode.getUserId(), 
            authCode.getScope()
        );
    }
}

Critical-Path Method Implementation

@Transactional
public Token authenticateUser(String username, String password) {
    // Validate input parameters
    if (username == null || password == null) {
        throw new InvalidRequestException("Username and password are required");
    }
    
    // Authenticate user credentials
    User user = userService.validateCredentials(username, password);
    if (user == null) {
        throw new AuthenticationException("Invalid credentials");
    }
    
    // Check if user is active
    if (!user.isActive()) {
        throw new AuthenticationException("User account is inactive");
    }
    
    // Generate access and refresh tokens
    Token accessToken = tokenService.generateAccessToken(user, "read write");
    Token refreshToken = tokenService.generateRefreshToken(user);
    
    // Store tokens in database
    tokenRepository.save(accessToken);
    tokenRepository.save(refreshToken);
    
    // Publish events for observability
    eventPublisher.publishTokenGeneratedEvent(accessToken);
    eventPublisher.publishTokenGeneratedEvent(refreshToken);
    
    // Return the access token for immediate use
    return accessToken;
}

Transaction Boundary Design

Single transaction encompasses:

  1. User authentication validation
  2. Token generation and persistence
  3. Event publishing (if using same transaction)
  4. Session invalidation for concurrent logins

Service Interface Contracts

public interface OAuthService {
    Token authenticateUser(String username, String password);
    Token validateToken(String token);
    void revokeToken(String token);
    Token refreshToken(String refreshToken);
}

Saga / Orchestration Pattern

For complex flows like refresh token rotation, a saga pattern is used to ensure consistency across distributed systems.

Domain Object / Value Object Design

public class Token {
    private final UUID id;
    private final String value;
    private final TokenType type;
    private final UUID userId;
    private final UUID clientId;
    private final Instant expiresAt;
    private final Instant createdAt;
    private Instant revokedAt;
    
    public Token(UUID id, String value, TokenType type, UUID userId, 
                 UUID clientId, Instant expiresAt, Instant createdAt) {
        this.id = id;
        this.value = value;
        this.type = type;
        this.userId = userId;
        this.clientId = clientId;
        this.expiresAt = expiresAt;
        this.createdAt = createdAt;
    }
    
    public boolean isExpired() {
        return Instant.now().isAfter(expiresAt);
    }
    
    public boolean isRevoked() {
        return revokedAt != null;
    }
    
    public void revoke() {
        this.revokedAt = Instant.now();
    }
}

public enum TokenType {
    ACCESS_TOKEN,
    REFRESH_TOKEN
}

5. Concurrency & Thread Safety

Concurrency Model

The OAuth service will use a thread pool-based concurrency model with Java's CompletableFuture and ExecutorService for handling asynchronous operations. The service will leverage the Reactor framework (via Spring WebFlux) for non-blocking I/O operations, allowing efficient handling of concurrent authentication requests without blocking threads. For synchronous operations, the service will use a fixed-size thread pool with size tuned based on CPU cores and I/O characteristics.

Race Condition Prevention Strategy

Race conditions will be prevented through:

  1. Database-level atomic operations for token generation and refresh
  2. Optimistic locking using version fields in entities to detect concurrent modifications
  3. Database transactions with proper isolation levels (READ_COMMITTED or REPEATABLE_READ)
  4. Application-level synchronization for critical sections using ReentrantLock or StampedLock where necessary

Optimistic vs. Pessimistic Locking Selection

Optimistic locking is selected for most operations because:

  • High read-to-write ratio in OAuth flows (token validation is more frequent than creation)
  • Lower contention and better scalability
  • Reduced blocking overhead in concurrent environments

Pessimistic locking is used for:

  • Token revocation operations where immediate consistency is required
  • Session creation/deletion operations that must not overlap

Deadlock Prevention Strategy

Deadlock prevention will be implemented through:

  1. Lock ordering - All operations acquire locks in a consistent order (e.g., by client ID, then user ID)
  2. Timeout-based acquisition - Locks will timeout after 50ms to prevent indefinite blocking
  3. Hierarchical lock management - Locks will be grouped by scope (client, user, session)
  4. Deadlock detection - Periodic check of lock wait chains with automatic timeout handling

Idempotency Implementation for Concurrent Duplicate Requests

Idempotency will be handled through:

  1. Idempotency keys for token creation endpoints (e.g., POST /oauth/token)
  2. Database uniqueness constraints on idempotency key + operation type combinations
  3. Cache-based idempotency - Store recent idempotency keys in Redis with TTL of 5 minutes
  4. Request deduplication - Use distributed locks to ensure only one request processes per idempotency key
// Example implementation of idempotent token creation with optimistic locking
public class OAuthTokenService {
    private final IdempotencyCache idempotencyCache;
    private final TokenRepository tokenRepository;
    private final LockManager lockManager;

    public OAuthTokenResponse createTokenWithIdempotency(
            TokenRequest request, 
            String idempotencyKey) {
        
        // Check if this operation was already completed
        String cachedResponse = idempotencyCache.get(idempotencyKey);
        if (cachedResponse != null) {
            return parseCachedResponse(cachedResponse);
        }

        // Acquire distributed lock for idempotency key
        try (DistributedLock lock = lockManager.acquire(idempotencyKey, 5000)) {
            // Double-check cache after acquiring lock
            cachedResponse = idempotencyCache.get(idempotencyKey);
            if (cachedResponse != null) {
                return parseCachedResponse(cachedResponse);
            }

            // Perform the actual token creation with optimistic locking
            OAuthToken token = createTokenWithOptimisticLocking(request);
            
            // Cache response for idempotency
            String response = serializeResponse(token);
            idempotencyCache.put(idempotencyKey, response, Duration.ofMinutes(5));
            
            return token;
        } catch (LockAcquisitionException e) {
            throw new IdempotencyException("Request already being processed");
        }
    }

    private OAuthToken createTokenWithOptimisticLocking(TokenRequest request) {
        // Use database transaction with version field for optimistic locking
        return tokenRepository.withTransaction(() -> {
            // Validate request and check existing tokens
            OAuthToken existing = tokenRepository.findByClientAndUser(
                request.getClientId(), 
                request.getUserId()
            );
            
            if (existing != null && !isExpired(existing)) {
                return existing;
            }
            
            // Create new token with version tracking
            OAuthToken newToken = createNewToken(request);
            
            // Update with optimistic locking - will throw exception if version changed
            tokenRepository.saveWithOptimisticLocking(newToken);
            
            return newToken;
        });
    }
}

6. Sequence Diagrams for Key Flows

sequenceDiagram
    participant U as User
    participant OAuth as OAuth Service
    participant DB as Database
    participant AuthN as AuthN Provider
    participant AuthZ as AuthZ Service

    U->>OAuth: 1. Initiate OAuth Flow (GET /authorize)
    OAuth->>U: 2. Redirect to AuthN with params
    U->>AuthN: 3. User authenticates
    AuthN->>OAuth: 4. Redirect back with code
    OAuth->>DB: 5. Validate and store auth code
    OAuth->>AuthN: 6. Exchange code for tokens
    AuthN-->>OAuth: 7. Access and Refresh Tokens
    OAuth->>DB: 8. Store tokens and user info
    OAuth->>U: 9. Return access token to client

    Note over U,OAuth: Happy Path - Successful Authorization Flow
sequenceDiagram
    participant U as User
    participant OAuth as OAuth Service
    participant DB as Database
    participant AuthN as AuthN Provider

    U->>OAuth: 1. Request with invalid code (POST /token)
    OAuth->>DB: 2. Validate auth code
    DB-->>OAuth: 3. Code not found or expired
    OAuth->>U: 4. Return 400 Bad Request with error details

    Note over U,OAuth: Error Path - Invalid Auth Code
sequenceDiagram
    participant Client as OAuth Client
    participant OAuth as OAuth Service
    participant DB as Database
    participant TokenService as Token Validation Service

    Client->>OAuth: 1. Request with access token (GET /resource)
    OAuth->>TokenService: 2. Validate JWT token
    TokenService->>DB: 3. Check if token is revoked
    DB-->>TokenService: 4. Token not found in revocation list
    TokenService-->>OAuth: 5. Token is valid and active
    OAuth->>Client: 6. Return protected resource

    Note over Client,OAuth: Async Flow - Token Validation and Resource Access

7. Caching Design

Cache Strategy Selection

For an OAuth service, we'll use cache-aside pattern with write-through for critical session data and cache-aside for less critical metadata. This approach provides the best balance between consistency, performance, and complexity.

Cache Key Design

Cache keys will follow a namespace strategy to avoid collisions and enable easy invalidation:

public class CacheKeyBuilder {
    public static String buildTokenKey(String tokenType, String tokenId) {
        return String.format("oauth:%s:%s", tokenType, tokenId);
    }
    
    public static String buildUserSessionKey(String userId) {
        return String.format("oauth:sessions:user:%s", userId);
    }
    
    public static String buildClientKey(String clientId) {
        return String.format("oauth:client:%s", clientId);
    }
    
    public static String buildTokenInfoKey(String token) {
        return String.format("oauth:token_info:%s", token);
    }
}

TTL Configuration

Data TypeTTLReason
Access Tokens3600s (1 hour)Standard OAuth2 token lifetime
Refresh Tokens86400s (24 hours)Longer-lived for refresh capability
User Sessions1800s (30 minutes)Session timeout for security
Client Metadata3600s (1 hour)Relatively static client info
Token Info Cache300s (5 minutes)Frequent access but not critical

Eviction Policy

We'll use LRU (Least Recently Used) eviction policy with a secondary TTL-based cleanup to handle edge cases where LRU might not be optimal for time-sensitive data.

Cache Stampede Prevention

To prevent cache stampedes:

  1. Probabilistic early expiry: Set cache TTL to 80% of actual TTL and use a random jitter factor
  2. Distributed locks: Use Redis distributed locks for cache population operations
public class CacheStampedePrevention {
    private static final double JITTER_FACTOR = 0.1;
    
    public boolean acquireLock(String key, int timeoutSeconds) {
        String lockKey = "lock:" + key;
        String lockValue = UUID.randomUUID().toString();
        
        // Try to acquire distributed lock
        boolean acquired = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, lockValue, timeoutSeconds, TimeUnit.SECONDS);
        
        return acquired;
    }
    
    public void releaseLock(String key, String lockValue) {
        String lockKey = "lock:" + key;
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), 
                             Collections.singletonList(lockKey), lockValue);
    }
}

Distributed Cache Architecture

We'll use Redis Cluster with:

  • 3 master nodes for high availability
  • 3 replica nodes per master for read scaling
  • Redis Sentinel for automatic failover monitoring
  • Cache sharding based on token type to distribute load

Cache Invalidation Strategy

We'll implement event-driven invalidation for critical data:

@Component
public class OAuthCacheInvalidator {
    
    @EventListener
    public void handleTokenRevocation(TokenRevokedEvent event) {
        String tokenKey = CacheKeyBuilder.buildTokenKey("access", event.getTokenId());
        redisTemplate.delete(tokenKey);
        
        // Invalidate associated session if exists
        String sessionKey = CacheKeyBuilder.buildUserSessionKey(event.getUserId());
        redisTemplate.delete(sessionKey);
    }
    
    @EventListener
    public void handleClientUpdate(ClientUpdatedEvent event) {
        String clientKey = CacheKeyBuilder.buildClientKey(event.getClientId());
        redisTemplate.delete(clientKey);
    }
}

Cache Hit/Miss Metrics

We'll track cache performance with:

  • oauth.cache.hits (counter)
  • oauth.cache.misses (counter)
  • oauth.cache.hit_ratio (gauge)
@Component
public class CacheMetrics {
    private final MeterRegistry meterRegistry;
    
    public void recordCacheHit(String cacheType) {
        Counter.builder("oauth.cache.hits")
            .tag("type", cacheType)
            .register(meterRegistry)
            .increment();
    }
    
    public void recordCacheMiss(String cacheType) {
        Counter.builder("oauth.cache.misses")
            .tag("type", cacheType)
            .register(meterRegistry)
            .increment();
    }
}

8. Repository / Data Access Layer

Repository Pattern Implementation

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

public interface UserRepository {
    Optional<User> findById(UUID id);
    Optional<User> findByEmail(String email);
    User save(User user);
    void delete(UUID id);
    List<User> findAll();
}

public interface OAuthClientRepository {
    Optional<OAuthClient> findById(String id);
    List<OAuthClient> findByUserId(UUID userId);
    OAuthClient save(OAuthClient client);
    void delete(String id);
}

public interface AccessTokenRepository {
    Optional<AccessToken> findByToken(String token);
    List<AccessToken> findByUserId(UUID userId);
    AccessToken save(AccessToken token);
    void delete(String token);
    List<AccessToken> findValidByUserId(UUID userId);
}

ORM vs. Raw Query Decisions

We use JPA/Hibernate for most entities due to its maturity, type safety, and ease of maintenance. However, complex analytical queries or performance-critical operations use raw SQL:

@Repository
public class AccessTokenRepositoryImpl implements AccessTokenRepository {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public List<AccessToken> findValidByUserId(UUID userId) {
        // Using JPA for simple queries
        return entityManager.createQuery(
            "SELECT t FROM AccessToken t WHERE t.userId = :userId AND t.expiry > CURRENT_TIMESTAMP",
            AccessToken.class)
            .setParameter("userId", userId)
            .getResultList();
    }
    
    @Override
    public List<AccessToken> findExpiredTokens(LocalDateTime cutoff) {
        // Using raw SQL for performance-critical batch operations
        String sql = """
            SELECT * FROM access_tokens 
            WHERE expiry < :cutoff AND revoked = false
            ORDER BY expiry ASC
            LIMIT 1000
            """;
        
        return entityManager.createNativeQuery(sql, AccessToken.class)
            .setParameter("cutoff", cutoff)
            .getResultList();
    }
}

Read vs. Write Separation

We implement CQRS for read-heavy operations to optimize performance:

// Read model - optimized for querying
public class OAuthClientReadModel {
    private String id;
    private String name;
    private String redirectUri;
    private boolean enabled;
    
    // getters and setters
}

// Write model - for mutations
public class OAuthClient {
    private String id;
    private String name;
    private String secret;
    private String redirectUri;
    private boolean enabled;
    private LocalDateTime createdAt;
    
    // getters and setters
}

// Read repository - optimized for reads
@Repository
public class OAuthClientReadRepository {
    @Query("SELECT new com.example.oauth.model.OAuthClientReadModel(c.id, c.name, c.redirectUri, c.enabled) " +
           "FROM OAuthClient c WHERE c.userId = :userId")
    List<OAuthClientReadModel> findByUserId(@Param("userId") UUID userId);
}

Connection Pool Configuration

We configure HikariCP with optimal settings for high-concurrency OAuth operations:

spring:
  datasource:
    hikari:
      maximum-pool-size: 50
      minimum-idle: 10
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      validation-timeout: 5000
      leak-detection-threshold: 60000

N+1 Query Prevention Strategy

We implement eager loading and batch fetching to prevent N+1 issues:

@Entity
@Table(name = "oauth_clients")
@NamedEntityGraph(
    name = "OAuthClient.withUserAndTokens",
    attributeNodes = @NamedAttributeNode(value = "user", subgraph = "tokens"),
    subgraphs = @NamedSubgraph(
        name = "tokens",
        attributeNodes = @NamedAttributeNode("accessTokens")
    )
)
public class OAuthClient {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
    
    @OneToMany(mappedBy = "client", fetch = FetchType.LAZY)
    @BatchSize(size = 20)
    private List<AccessToken> accessTokens = new ArrayList<>();
    
    // getters and setters
}

// Query that fetches all related data in minimal queries:
@Query("SELECT c FROM OAuthClient c JOIN FETCH c.user u LEFT JOIN FETCH c.accessTokens t WHERE c.userId = :userId")
List<OAuthClient> findByUserIdWithTokens(@Param("userId") UUID userId);

Batch Operation Design

For bulk operations like token cleanup, we implement batch processing:

@Service
@Transactional
public class AccessTokenBatchService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public int cleanupExpiredTokens(LocalDateTime cutoff, int batchSize) {
        String sql = """
            DELETE FROM access_tokens 
            WHERE expiry < :cutoff AND revoked = false
            """;
            
        return entityManager.createNativeQuery(sql)
            .setParameter("cutoff", cutoff)
            .setMaxResults(batchSize)
            .executeUpdate();
    }
    
    public List<AccessToken> findExpiredTokensBatch(LocalDateTime cutoff, int batchSize) {
        String sql = """
            SELECT * FROM access_tokens 
            WHERE expiry < :cutoff AND revoked = false
            ORDER BY expiry ASC
            LIMIT :batchSize
            """;
            
        return entityManager.createNativeQuery(sql, AccessToken.class)
            .setParameter("cutoff", cutoff)
            .setParameter("batchSize", batchSize)
            .getResultList();
    }
}

9. Asynchronous Processing & Messaging

Event-driven boundaries in this feature

The OAuth service will use event-driven architecture for asynchronous operations that don't need to block the main request flow. The key boundaries are:

  • Token refresh and revocation: When a refresh token is used or a token is explicitly revoked, we need to invalidate related sessions and notify other services that may be using these tokens.
  • User session management: When a user's account is updated (e.g., password change), we need to invalidate all active sessions for that user.
  • Audit logging: All token operations (creation, refresh, revocation) should be logged asynchronously for compliance and security monitoring.
  • Token expiration: When tokens expire, we may want to clean up related resources or send notifications.

Synchronous operations include:

  • Token generation (authorization code, implicit, client credentials)
  • Token validation
  • User authentication flow

Message schema design with sample payload

We'll use JSON for message payloads with versioning strategy:

{
  "version": "1.0",
  "event_type": "oauth.token.revoked",
  "timestamp": "2023-10-15T14:30:00Z",
  "payload": {
    "token_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "user_id": "user-123",
    "client_id": "client-456",
    "token_type": "refresh",
    "expires_at": "2023-10-15T15:30:00Z"
  },
  "source": "oauth-service",
  "correlation_id": "correlation-789"
}

Dead letter queue strategy and poison message handling

For failed message processing, we'll implement a dead-letter queue (DLQ) strategy:

  1. Retry policy: Messages will be retried up to 3 times with exponential backoff (1s, 4s, 9s)
  2. DLQ handling: Messages that fail after max retries will be sent to a DLQ topic
  3. Manual intervention: Failed messages in DLQ will trigger alerts and require manual review

At-least-once vs. exactly-once delivery

We'll use at-least-once delivery for the following reasons:

  • Token operations are idempotent (reprocessing a token revocation doesn't cause harm)
  • Simpler implementation and better performance
  • Our system can handle duplicate events gracefully

Retry policy for consumers

public class RetryPolicy {
    private static final int MAX_RETRIES = 3;
    private static final long INITIAL_DELAY_MS = 1000;
    private static final double BACKOFF_MULTIPLIER = 4.0;
    
    public boolean shouldRetry(int attempt, Exception error) {
        if (attempt >= MAX_RETRIES) return false;
        
        // Don't retry on certain error types
        if (error instanceof IllegalArgumentException || 
            error instanceof InvalidTokenException) {
            return false;
        }
        
        return true;
    }
    
    public long getDelay(int attempt) {
        return (long) Math.pow(BACKOFF_MULTIPLIER, attempt) * INITIAL_DELAY_MS;
    }
}

Outbox pattern for reliable event publishing

We'll implement the outbox pattern to ensure message delivery reliability:

@Entity
@Table(name = "oauth_events")
public class OAuthEvent {
    @Id
    private UUID id;
    
    @Column(name = "event_type")
    private String eventType;
    
    @Column(name = "payload")
    private String payload;
    
    @Column(name = "status")
    private EventStatus status = EventStatus.PENDING;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt = LocalDateTime.now();
    
    @Column(name = "processed_at")
    private LocalDateTime processedAt;
    
    public enum EventStatus {
        PENDING, PROCESSED, FAILED
    }
}

Sequence diagram for async flows

sequenceDiagram
    participant Client
    participant OAuthService
    participant EventPublisher
    participant MessageBroker
    participant SessionManager
    participant AuditLogger
    
    Client->>OAuthService: Request token refresh
    OAuthService->>OAuthService: Validate refresh token
    OAuthService->>OAuthService: Generate new access token
    OAuthService->>EventPublisher: Publish token.revoked event
    EventPublisher->>MessageBroker: Publish to 'oauth.events' topic
    MessageBroker->>SessionManager: Deliver to session management service
    MessageBroker->>AuditLogger: Deliver to audit logging service
    
    Note over OAuthService,EventPublisher: Async processing - no response delay

The outbox pattern ensures that events are persisted before being published to the message broker, preventing data loss in case of system failures during event publishing. The message broker handles delivery to downstream services with guaranteed delivery semantics.

The async flows are designed to handle burst loads and provide better user experience by not blocking the main authentication flow. The audit logging and session invalidation happen asynchronously to maintain service responsiveness while ensuring compliance and security requirements are met.

10. Error Handling Design

Error Classification Hierarchy

The OAuth service follows a structured error classification system to enable proper handling and monitoring:

Error
├── ValidationException (400)
│   ├── InvalidRequestException
│   ├── InvalidClientException
│   └── InvalidScopeException
├── AuthenticationException (401)
│   ├── InvalidTokenException
│   └── InsufficientScopeException
├── AuthorizationException (403)
│   ├── AccessDeniedException
│   └── PermissionDeniedException
├── ResourceNotFoundException (404)
│   ├── ClientNotFoundException
│   └── UserNotFoundException
├── ConflictException (409)
│   ├── DuplicateClientException
│   └── TokenAlreadyIssuedException
├── RateLimitException (429)
│   └── TooManyRequestsException
├── InternalServerException (500)
│   ├── DatabaseException
│   ├── ExternalServiceException
│   └── UnknownException
└── ServiceUnavailableException (503)
    └── MaintenanceModeException

Error Propagation Strategy

  • Domain-specific errors: Caught and converted to appropriate OAuth error types before being propagated up the stack
  • Infrastructure errors: Bubble up as InternalServerException or ServiceUnavailableException for transparent logging and alerting
  • Validation errors: Caught early in the API layer and converted to InvalidRequestException with appropriate error codes

Retry Strategy

For external service calls (e.g., database, cache):

public class RetryableOperation {
    private static final int MAX_RETRIES = 3;
    private static final long BASE_DELAY_MS = 1000;
    private static final double JITTER_FACTOR = 0.2;

    public <T> T executeWithRetry(Supplier<T> operation) {
        int attempts = 0;
        while (true) {
            try {
                return operation.get();
            } catch (DatabaseException | ExternalServiceException e) {
                attempts++;
                if (attempts >= MAX_RETRIES) {
                    throw new ServiceUnavailableException("Operation failed after " + MAX_RETRIES + " attempts", e);
                }
                
                long delay = (long) (BASE_DELAY_MS * Math.pow(2, attempts - 1));
                delay += (long) (delay * JITTER_FACTOR * (Math.random() - 0.5) * 2);
                
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new ServiceUnavailableException("Retry interrupted", ie);
                }
            }
        }
    }
}

Circuit Breaker Design

public class CircuitBreaker {
    private final int failureThreshold;
    private final long timeout;
    private final int resetTimeout;
    
    private volatile CircuitState state = CircuitState.CLOSED;
    private volatile int failureCount = 0;
    private volatile long lastFailureTime = 0;

    public enum CircuitState {
        CLOSED, OPEN, HALF_OPEN
    }

    public <T> T execute(Supplier<T> operation) {
        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 > resetTimeout) {
                    state = CircuitState.HALF_OPEN;
                    return operation.get();
                }
                throw new ServiceUnavailableException("Circuit breaker 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("Unexpected circuit state");
    }

    private void onSuccess() {
        failureCount = 0;
    }

    private void onFailure(Exception e) {
        failureCount++;
        lastFailureTime = System.currentTimeMillis();
        
        if (failureCount >= failureThreshold) {
            state = CircuitState.OPEN;
        }
    }
}

Fallback Behavior Design

public class OAuthServiceFallback {
    private final Cache<String, Client> clientCache;
    private final RedisTemplate<String, String> redisTemplate;

    public Client getClientWithFallback(String clientId) {
        try {
            return clientService.getClient(clientId);
        } catch (DatabaseException | ServiceUnavailableException e) {
            // Fallback to cache or default client
            Client cached = clientCache.getIfPresent(clientId);
            if (cached != null) {
                return cached;
            }
            
            // Fallback to default client or throw error
            return createDefaultClient();
        }
    }

    public TokenResponse generateTokenWithFallback(TokenRequest request) {
        try {
            return tokenService.generateToken(request);
        } catch (DatabaseException | ExternalServiceException e) {
            // Log error and return fallback response
            logger.warn("Token generation failed, returning fallback response", e);
            return TokenResponse.builder()
                .error("server_error")
                .errorDescription("Temporary service unavailability")
                .build();
        }
    }
}

Error Logging Format

public class OAuthErrorLogger {
    private static final Logger logger = LoggerFactory.getLogger(OAuthErrorLogger.class);

    public void logOAuthError(String errorCode, String errorDescription, 
                             Map<String, Object> context) {
        Map<String, Object> logContext = new HashMap<>();
        logContext.put("error_code", errorCode);
        logContext.put("error_description", errorDescription);
        logContext.put("timestamp", Instant.now().toString());
        logContext.put("trace_id", MDC.get("trace_id"));
        logContext.put("span_id", MDC.get("span_id"));
        logContext.put("user_id", context.get("user_id"));
        logContext.put("client_id", context.get("client_id"));
        logContext.put("request_id", context.get("request_id"));
        logContext.put("ip_address", context.get("ip_address"));
        logContext.put("error_stack_trace", getStackTrace(context.get("exception")));

        logger.error("OAuth Error: {}", logContext);
    }

    private String getStackTrace(Object exception) {
        if (exception instanceof Throwable) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            ((Throwable) exception).printStackTrace(pw);
            return sw.toString();
        }
        return "No stack trace available";
    }
}

Alerting Thresholds

Error TypeAlert ThresholdSeverity
DatabaseException> 10 errors/minute for 5 minutesCRITICAL
ExternalServiceException> 5 errors/minute for 3 minutesMAJOR
InvalidTokenException> 100 errors/minute for 1 minuteMINOR
RateLimitException> 500 errors/minute for 1 minuteWARNING
ServiceUnavailableException> 50 errors/minute for 2 minutesCRITICAL

Error Response Format

All error responses follow the OAuth 2.0 specification:

{
  "error": "invalid_request",
  "error_description": "The request is missing a required parameter, includes an invalid parameter value, or is otherwise malformed.",
  "error_uri": "https://docs.example.com/oauth/errors#invalid_request",
  "state": "abc123"
}

Error Handling in Service Layer

@Service
public class OAuthTokenService {
    private final TokenRepository tokenRepository;
    private final ClientRepository clientRepository;
    private final OAuthErrorLogger errorLogger;

    public TokenResponse generateToken(TokenRequest request) {
        try {
            // Validate input
            validateTokenRequest(request);
            
            // Fetch client
            Client client = clientRepository.findByClientId(request.getClientId())
                .orElseThrow(() -> new InvalidClientException("Client not found"));
            
            // Validate client credentials
            if (!validateClientSecret(client, request.getClientSecret())) {
                throw new InvalidClientException("Invalid client credentials");
            }
            
            // Validate scope
            if (!validateScope(client, request.getScope())) {
                throw new InvalidScopeException("Invalid scope requested");
            }
            
            // Generate token
            Token token = generateAndStoreToken(client, request);
            
            return TokenResponse.builder()
                .accessToken(token.getAccessToken())
                .tokenType("Bearer")
                .expiresIn(token.getExpiresIn())
                .scope(token.getScope())
                .build();
                
        } catch (InvalidRequestException | InvalidClientException | 
                 InvalidScopeException e) {
            errorLogger.logOAuthError(e.getErrorCode(), e.getMessage(), 
                buildErrorContext(request));
            throw e;
        } catch (DatabaseException | ExternalServiceException e) {
            errorLogger.logOAuthError("server_error", "Internal server error", 
                buildErrorContext(request));
            throw new ServiceUnavailableException("Service temporarily unavailable", e);
        }
    }
}

Error Budget Considerations

  • Error Budget: 2% of total requests (100k requests = 2k errors)
  • Budget Consumption: Errors that impact user experience or system stability count toward the budget
  • Alerting: When error rate exceeds 1% of total requests, trigger on-call alert
  • Grace Period: Allow 5 minutes of error accumulation before triggering alerts

11. Security Design

Input validation and sanitization strategy

All inputs are validated using a combination of:

  • Regex-based validation for format constraints (e.g., email, URI patterns)
  • Length limits to prevent buffer overflow attacks
  • Type checking with strict parsing (e.g., UUID.fromString() for IDs)
  • Custom validation rules using Apache Commons Validator or similar libraries
  • SQL injection prevention through parameterized queries and ORM usage

SQL injection prevention

  • All database queries are constructed using parameterized/prepared statements
  • No dynamic SQL string concatenation
  • ORM is used for all data access (e.g., Hibernate/JPA)
  • Stored procedures are avoided in favor of parameterized queries

Authentication middleware design

  • JWT-based authentication with HS256 signing algorithm (symmetric key)
  • Middleware intercepts all protected routes and validates JWT tokens
  • Token expiration is enforced via exp claim (default 1 hour)
  • Refresh token rotation with short-lived access tokens (1 hour) and long-lived refresh tokens (30 days)
  • Token revocation via Redis-based blacklisting (short TTL of 1 hour for revoked tokens)

Authorization enforcement

  • Role-Based Access Control (RBAC) is implemented at service layer:
    • Roles: ADMIN, USER, SERVICE
    • Permissions are mapped to roles via a permission matrix
    • Authorization checks performed in service layer before operation execution
  • Resource-level permissions are enforced for sensitive operations (e.g., admin actions on users)
  • ABAC is considered for complex policies but not implemented due to complexity and current scope

Sensitive data encryption at rest and in transit

  • At rest: AES-256 encryption using AWS KMS or similar managed key service
  • In transit: TLS 1.3 with HSTS header enforcement (max-age 31536000)
  • Secrets storage: AWS Secrets Manager or Vault for storing client secrets and signing keys
  • Token encryption: Access tokens are encrypted at rest using AES-256-GCM

PII handling and masking strategy

  • PII fields (email, phone) are encrypted at rest using AES-256
  • PII in logs is masked (e.g., user@example.com → u***@example.com)
  • Log format includes trace_id and span_id but excludes sensitive fields
  • Audit logs capture only necessary PII for compliance (e.g., user login, consent changes)

Secrets management

  • Secrets stored in AWS Secrets Manager or HashiCorp Vault
  • Rotation policy: Client secrets rotated every 90 days
  • Access control via IAM roles or Vault policies to restrict access to secrets
  • Environment-specific keys for dev/staging/prod environments

Audit logging design

  • Events logged:
    • User login/logout
    • Token generation/revocation
    • Client registration/update
    • Admin actions (user deletion, role changes)
  • Log format: JSON with fields:
    {
      "timestamp": "2023-10-15T14:30:00Z",
      "userId": "uuid",
      "action": "TOKEN_GENERATED",
      "ipAddress": "192.168.1.100",
      "userAgent": "Mozilla/5.0...",
      "traceId": "abc123",
      "sessionId": "session-uuid"
    }
  • Retention policy: 90 days for audit logs, archived to S3 after 30 days
  • Compliance: Logs are retained for audit purposes per SOC2 and GDPR requirements

12. Testing Strategy

Unit Testing Scope and Coverage Targets

Unit testing will cover 85%+ of business logic with 90%+ line coverage for core service classes. Each domain object will have dedicated unit tests covering all state transitions and validation logic. Service layer methods will be tested in isolation with mocked dependencies, focusing on:

  • Authentication flow validation (token generation, refresh, revocation)
  • Authorization checks and permission validation
  • Token lifecycle management (expiry, renewal, invalidation)
  • Error condition handling (expired tokens, malformed requests)
  • Business rule enforcement (rate limits, scope validation)

Integration Testing Strategy

Integration tests will validate end-to-end flows using embedded databases (H2 for unit tests, HSQLDB for integration) and mock external services:

  • Database persistence layer testing with real SQL schema
  • OAuth2 flow end-to-end validation (authorization code, implicit, client credentials)
  • Token exchange and refresh scenarios
  • External provider integration testing (Google, GitHub) with WireMock
  • Multi-tenant token validation across different client configurations

Contract Testing

Contract testing will be implemented using Pact for service-to-service contracts between OAuth service and downstream consumers. This ensures that:

  • Token validation endpoints maintain consistent response formats
  • Authorization header parsing follows agreed specifications
  • Error responses match defined schemas across all endpoints
  • Client credentials validation maintains contract integrity

Performance Testing Tool and Key Scenarios

Performance testing will use k6 with the following key scenarios:

  • Concurrent token generation: 1000 concurrent requests generating access tokens
  • Token refresh throughput: 500 concurrent refresh operations per second
  • Authorization header validation: 2000 concurrent requests validating existing tokens
  • Database connection pool stress test: 500 concurrent database operations

Test Data Management Strategy

Test data will be managed through:

  • Factory patterns for generating valid OAuth client configurations
  • Fixture files containing pre-defined test users, clients, and scopes
  • Seed scripts for initializing test database state with realistic data sets
  • Data cleanup procedures to ensure test isolation between runs
  • Parameterized test cases for edge case validation (empty scopes, invalid grants, malformed tokens)

The testing strategy prioritizes coverage of authentication flows, token security validation, and concurrent request handling to ensure the OAuth service can handle production-scale usage while maintaining security guarantees.

13. Observability Design

Structured Logging Schema

The logging system will use structured JSON format with consistent fields across all services to enable effective log aggregation and analysis. The schema includes:

{
  "timestamp": "2023-10-15T14:30:45.123Z",
  "level": "INFO",
  "service": "oauth-service",
  "trace_id": "1234567890abcdef1234567890abcdef",
  "span_id": "abcdef1234567890abcdef1234567890",
  "user_id": "user-123",
  "entity_id": "client-456",
  "request_id": "req-789",
  "operation": "token_exchange",
  "message": "Token exchange successful",
  "duration_ms": 45,
  "error_code": "INVALID_GRANT",
  "error_message": "Invalid grant type",
  "http_method": "POST",
  "http_path": "/oauth/token",
  "http_status": 400,
  "ip_address": "192.168.1.1",
  "user_agent": "Mozilla/5.0...",
  "jwt_token_id": "token-abc123"
}

PII masking rules:

  • User IDs and email addresses are masked in logs (e.g., user-123 → user-***)
  • JWT tokens are truncated (e.g., eyJhbGciOiJIUzI1NiIs... → eyJhbGciOiJIUz...)
  • IP addresses are masked to network level (e.g., 192.168.1.1 → 192.168.0.0)

Distributed Tracing Instrumentation

All key operations will be instrumented with OpenTelemetry to enable end-to-end tracing:

Custom spans to add:

  1. oauth.token_exchange - Token exchange flow
  2. oauth.authorization_code_flow - Authorization code grant flow
  3. oauth.client_authentication - Client credential validation
  4. oauth.token_validation - JWT token verification
  5. oauth.refresh_token_flow - Refresh token exchange

Attributes to capture per key operation:

Span currentSpan = Span.current();
currentSpan.setAttribute("oauth.client_id", clientId);
currentSpan.setAttribute("oauth.grant_type", grantType);
currentSpan.setAttribute("oauth.token_type", tokenType);
currentSpan.setAttribute("oauth.scopes", String.join(",", scopes));
currentSpan.setAttribute("oauth.user_id", userId);
currentSpan.setAttribute("oauth.is_refresh_token", isRefreshToken);

Metrics Design

RED Method Metrics (Rate, Errors, Duration):

Metric NameTypeLabelsDescription
oauth_requests_totalCountermethod, path, status_codeTotal request count
oauth_requests_duration_secondsHistogrammethod, path, status_codeRequest processing time
oauth_requests_errors_totalCountermethod, path, error_typeRequest error count
oauth_token_exchanges_totalCountergrant_type, client_idToken exchange count
oauth_token_validation_failures_totalCountervalidation_type, error_codeToken validation failures
oauth_active_sessions_gaugeGaugeclient_idActive session count

Health Check Endpoint Design

@RestController
@RequestMapping("/health")
public class HealthController {
    
    @GetMapping("/live")
    public ResponseEntity<Map<String, Object>> live() {
        Map<String, Object> response = new HashMap<>();
        response.put("status", "healthy");
        response.put("timestamp", Instant.now());
        return ResponseEntity.ok(response);
    }
    
    @GetMapping("/ready")
    public ResponseEntity<Map<String, Object>> ready() {
        Map<String, Object> response = new HashMap<>();
        response.put("status", "ready");
        response.put("timestamp", Instant.now());
        response.put("database", databaseHealth());
        response.put("cache", cacheHealth());
        response.put("external_services", externalServicesHealth());
        return ResponseEntity.ok(response);
    }
    
    @GetMapping("/startup")
    public ResponseEntity<Map<String, Object>> startup() {
        Map<String, Object> response = new HashMap<>();
        response.put("status", "startup_complete");
        response.put("timestamp", Instant.now());
        response.put("dependencies", dependenciesStatus());
        return ResponseEntity.ok(response);
    }
}

Alerting Thresholds

Critical alerts (require on-call escalation):

  • oauth_requests_errors_total{status_code="5xx"} > 10 errors/minute for 5 minutes
  • oauth_requests_duration_seconds{percentile="99"} > 500ms for 1 minute
  • oauth_token_validation_failures_total > 50 failures/minute for 2 minutes
  • oauth_active_sessions_gauge > 100k concurrent sessions for 5 minutes

Warning alerts (require investigation):

  • oauth_requests_errors_total{status_code="4xx"} > 100 errors/minute for 5 minutes
  • oauth_requests_duration_seconds{percentile="95"} > 200ms for 1 minute
  • oauth_token_exchanges_total{grant_type="authorization_code"} < 50% of total for 10 minutes

On-call escalation path:

  1. Primary on-call engineer (OAuth service owner)
  2. Security team (for auth-related alerts)
  3. Platform engineering (infrastructure issues)

14. Open Questions, Risks & Trade-offs

Decisions requiring product / engineering alignment

  • Token format and compatibility: Should we support JWT-based tokens (for stateless validation) or opaque token format (for enhanced security and revocation capabilities)? This decision affects client-side implementation complexity and integration with existing systems.
  • Authorization server vs. resource server separation: Should the OAuth service handle both authorization and resource server responsibilities, or should it be strictly an authorization server that delegates resource validation to separate services?
  • Support for multiple grant types: Which OAuth 2.0 grant types should we support (Authorization Code, Implicit, Resource Owner Password Credentials, Client Credentials)? Supporting more grant types increases complexity but improves flexibility for different client types.

Known trade-offs and justifications

  • Token size vs. performance: JWT tokens are self-contained but can become large with extensive claims, increasing network overhead and storage requirements. Opaque tokens are compact but require database lookups for validation.
  • Stateless vs. stateful token validation: Stateless JWT validation provides better scalability but removes the ability to immediately revoke tokens. Stateful validation allows for immediate revocation but introduces state management complexity.
  • Database vs. in-memory caching: Using Redis for token storage provides persistence and distributed access but introduces network latency. In-memory storage offers better performance but lacks persistence across service restarts.
  • User session vs. token-based authentication: Session-based approaches provide better control over user sessions but require session storage and management. Token-based approaches are more scalable but less flexible for immediate session invalidation.

Risks and mitigation strategies

  • Token replay attacks: Risk of stolen tokens being reused. Mitigation includes short token expiration times, refresh token rotation, and implementing device fingerprinting.
  • Cross-site request forgery (CSRF): Risk of malicious sites making unauthorized requests on behalf of authenticated users. Mitigation includes CSRF token generation and validation, secure cookie attributes (HttpOnly, SameSite), and strict origin validation.
  • Token storage security: Risk of token exposure in logs or error messages. Mitigation includes comprehensive logging sanitization, secure token handling practices, and proper error response design.
  • Rate limiting bypass: Risk of malicious actors bypassing rate limits through multiple IP addresses or proxies. Mitigation includes IP-based rate limiting with adaptive thresholds, request pattern analysis, and integration with threat detection systems.
  • Data consistency in distributed environment: Risk of token state inconsistency across microservices. Mitigation includes implementing eventual consistency patterns, distributed transaction management, and proper cache invalidation strategies.

Technical debt acknowledged

  • Legacy protocol support: Supporting older OAuth 1.0 or deprecated grant types introduces technical debt and maintenance overhead that may not be immediately necessary but could complicate future upgrades.
  • Tight coupling between services: If the OAuth service is tightly coupled to specific client applications or resource servers, future flexibility in system architecture will be reduced.
  • Inconsistent error handling: Different error responses across endpoints can lead to inconsistent client-side handling and debugging difficulties.
  • Limited extensibility: Hard-coded token formats or limited customization options for claims and scopes may require significant refactoring in the future to support new use cases.
  • Monitoring gap: Initial implementation may lack comprehensive observability for token usage patterns, authentication success/failure rates, and user behavior analytics that would be valuable for security and operational insights.

Was this article helpful?

Topics in this article

More from Backend LLD

  • Backend LLD

    API Request Validation Framework Design

    Comprehensive low-level design for a robust, extensible API request validation framework with real-time validation and error handling capabilities.

  • Backend LLD

    JWT Token Management System Design

    Comprehensive low-level design for a secure JWT token management system with refresh token handling, blacklisting, and distributed session management.

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.