Skip to main content

Authorization Service Design: RBAC and ABAC Implementation

Comprehensive low-level design for a scalable authorization service supporting role-based and attribute-based access control with fine-grained permissions.

This document presents a production-ready low-level design for an authorization service that implements both role-based and attribute-based access control models, providing a scalable foundation for managing fine-grained permissions across distributed systems.

1. Problem Statement & Requirements

Clarifying questions

  1. What scale expectations are there for this authorization service? Will it handle millions of concurrent requests and thousands of users/roles?
  2. What consistency requirements exist for authorization decisions? Should they be strongly consistent or eventual consistency acceptable?
  3. What are the key entities in the system and their relationships? Specifically, how do users, roles, permissions, and resources relate to each other?
  4. What's the read/write ratio for authorization checks? Is it predominantly reads (e.g., 95% of traffic) or more balanced?
  5. What latency sensitivity do we have for authorization decisions? For example, should all checks complete under 50ms?
  6. What authentication model is in place? Is it JWT-based, OAuth2, or session-based?
  7. Are there any external systems that this authorization service integrates with? For example, SSO providers or identity management systems.

Functional requirements

  • User Role Assignment: Assign users to roles and manage role hierarchies.
  • Permission Management: Define, update, and delete permissions for roles.
  • Resource-Based Access Control: Support fine-grained access control based on resource types and identifiers.
  • ABAC Support: Evaluate policies based on user attributes, resource attributes, and environment context.
  • Authorization Decision Engine: Make authorization decisions for incoming requests.
  • Audit Logging: Log all authorization decisions with context and outcome.
  • Policy Management API: CRUD operations for managing ABAC policies.

Non-functional requirements

  • Latency Targets:
    • p50: < 20ms
    • p95: < 50ms
    • p99: < 100ms
  • Throughput: Support at least 50,000 authorization checks per second.
  • Availability: 99.9% uptime (5 minutes of downtime/month).
  • Scalability: Support horizontal scaling to handle peak loads.
  • Security: All authorization decisions must be auditable and tamper-proof.

Constraints and assumptions

  • Assumption: The system will be used within a single organization or application ecosystem.
  • Assumption: Users and roles are managed outside this service (e.g., via an identity provider).
  • Constraint: Authorization decisions must be made within 100ms to avoid impacting user experience.
  • Constraint: The service will be deployed in a microservices architecture with multiple clients.
  • Assumption: All authorization policies can fit into memory for fast evaluation.

Out of scope

  • User/Role Management: Creating or deleting users and roles outside the scope of this service.
  • Identity Provider Integration: Authentication integration (e.g., SAML, OAuth2) is out of scope.
  • Policy Language Implementation: Full policy engine implementation (e.g., XACML) is out of scope.
  • Cross-service Authorization: Authorization across different microservices or domains.
  • Real-time Policy Updates: Immediate propagation of policy changes to all instances is not required.

2. API Design

API Style Selection

REST is selected for this authorization service due to its simplicity, wide adoption, and clear semantics. It provides well-defined HTTP methods (GET, POST, PUT, DELETE) that align naturally with CRUD operations on authorization resources. REST also enables better tooling support and easier debugging compared to GraphQL or gRPC.

Endpoint Definitions

MethodPathDescriptionAuth
POST/v1/rolesCreate a new role with permissionsJWT + Admin
GET/v1/roles/{roleId}Retrieve role detailsJWT + Admin
PUT/v1/roles/{roleId}Update role permissionsJWT + Admin
DELETE/v1/roles/{roleId}Delete a roleJWT + Admin
POST/v1/users/{userId}/rolesAssign role to userJWT + Admin
DELETE/v1/users/{userId}/roles/{roleId}Remove role from userJWT + Admin
POST/v1/permissionsCreate new permissionJWT + Admin
GET/v1/permissions/{permissionId}Retrieve permission detailsJWT + Admin
GET/v1/users/{userId}/permissionsList user's permissionsJWT + User
GET/v1/users/{userId}/rolesList user's rolesJWT + User
POST/v1/evaluateEvaluate if user has permissionJWT + User
GET/v1/policiesList all ABAC policiesJWT + Admin

Request and Response Schemas

Create Role Request

{
  "name": "admin",
  "description": "Administrator role with full access",
  "permissions": ["read:user", "write:user", "delete:user"]
}

Create Role Response

{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "name": "admin",
  "description": "Administrator role with full access",
  "permissions": ["read:user", "write:user", "delete:user"],
  "createdAt": "2023-10-01T10:00:00Z",
  "updatedAt": "2023-10-01T10:00:00Z"
}

Evaluate Permission Request

{
  "userId": "user-123",
  "permission": "read:user"
}

Evaluate Permission Response

{
  "userId": "user-123",
  "permission": "read:user",
  "granted": true,
  "reason": "User has role admin which includes read:user permission"
}

HTTP Status Codes and Error Format

Standard HTTP status codes will be used:

  • 200 OK - Successful GET, PUT, PATCH
  • 201 Created - Successful POST
  • 400 Bad Request - Invalid request body or parameters
  • 401 Unauthorized - Missing or invalid auth token
  • 403 Forbidden - Insufficient permissions
  • 404 Not Found - Resource does not exist
  • 500 Internal Server Error - Unexpected server error

Standardized error response:

{
  "error": {
    "code": "INVALID_INPUT",
    "message": "Permission name must be unique and not empty",
    "details": {
      "field": "name",
      "value": ""
    }
  }
}

API Versioning Strategy

Versioned using path prefix (/v1/) to ensure backward compatibility and allow for future breaking changes. This approach is simple, explicit, and widely adopted in FAANG services.

Pagination Design

Cursor-based pagination will be used for listing endpoints to handle large datasets efficiently and provide stable results even when data changes between requests.

Authentication and Authorization

All endpoints will require JWT tokens for authentication. Admin-level access (creating roles, assigning permissions) will require a special "admin" scope in the token. User-level access (checking permissions) can be granted to any authenticated user.

Rate Limiting

  • 1000 requests per minute for admin endpoints
  • 5000 requests per minute for user permission check endpoints
  • Rate limit enforced via Redis with token bucket algorithm

Idempotency Design

Mutation endpoints will support idempotency keys via X-Idempotency-Key header:

POST /v1/roles HTTP/1.1
X-Idempotency-Key: user-123-role-admin-uuid

If the same key is used, the system will return the previous response without re-executing the operation.

3. Data Model Design

Entity-Relationship Diagram

erDiagram
    USER ||--o{ USER_ROLE : assigns
    ROLE ||--o{ USER_ROLE : grants
    ROLE ||--o{ ROLE_PERMISSION : contains
    PERMISSION ||--o{ ROLE_PERMISSION : grants
    USER ||--o{ USER_PERMISSION : inherits
    USER_PERMISSION ||--o{ USER_PERMISSION_ASSIGNMENT : applies
    ROLE ||--o{ ROLE_ASSIGNMENT : assigns
    USER ||--o{ ROLE_ASSIGNMENT : gets
    RESOURCE ||--o{ PERMISSION : protects
    USER ||--o{ USER_ATTRIBUTE : has
    ATTRIBUTE ||--o{ USER_ATTRIBUTE : describes
    ROLE ||--o{ ROLE_ATTRIBUTE : has
    ATTRIBUTE ||--o{ ROLE_ATTRIBUTE : describes
    USER ||--o{ USER_SESSION : maintains
    
    USER {
        string id PK
        string email
        string status
        timestamp created_at
        timestamp updated_at
    }
    
    ROLE {
        string id PK
        string name
        string description
        timestamp created_at
        timestamp updated_at
    }
    
    PERMISSION {
        string id PK
        string name
        string resource
        string action
        timestamp created_at
        timestamp updated_at
    }
    
    RESOURCE {
        string id PK
        string name
        string type
        timestamp created_at
        timestamp updated_at
    }
    
    ATTRIBUTE {
        string id PK
        string name
        string value_type
        timestamp created_at
        timestamp updated_at
    }
    
    USER_ROLE {
        string user_id FK
        string role_id FK
        timestamp created_at
    }
    
    ROLE_PERMISSION {
        string role_id FK
        string permission_id FK
        timestamp created_at
    }
    
    USER_PERMISSION {
        string user_id FK
        string permission_id FK
        timestamp created_at
    }
    
    USER_PERMISSION_ASSIGNMENT {
        string user_permission_id FK
        string resource_id FK
        string attribute_value
        timestamp created_at
    }
    
    ROLE_ASSIGNMENT {
        string user_id FK
        string role_id FK
        timestamp created_at
    }
    
    USER_ATTRIBUTE {
        string user_id FK
        string attribute_id FK
        string value
        timestamp created_at
    }
    
    ROLE_ATTRIBUTE {
        string role_id FK
        string attribute_id FK
        string value
        timestamp created_at
    }
    
    USER_SESSION {
        string user_id FK
        string session_id PK
        timestamp created_at
        timestamp expires_at
    }

Table / Collection Schema Definitions

Users Table

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    status VARCHAR(50) DEFAULT 'active',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_users_email (email),
    INDEX idx_users_status (status)
);

Roles Table

CREATE TABLE roles (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) UNIQUE NOT NULL,
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_roles_name (name)
);

Permissions Table

CREATE TABLE permissions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) UNIQUE NOT NULL,
    resource VARCHAR(255),
    action VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_permissions_resource_action (resource, action),
    INDEX idx_permissions_name (name)
);

Resources Table

CREATE TABLE resources (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) UNIQUE NOT NULL,
    type VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_resources_name (name),
    INDEX idx_resources_type (type)
);

Attributes Table

CREATE TABLE attributes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) UNIQUE NOT NULL,
    value_type VARCHAR(50),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_attributes_name (name)
);

User-Roles Assignment Table

CREATE TABLE user_roles (
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    PRIMARY KEY (user_id, role_id),
    INDEX idx_user_roles_user_id (user_id),
    INDEX idx_user_roles_role_id (role_id)
);

Role-Permissions Assignment Table

CREATE TABLE role_permissions (
    role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
    permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    PRIMARY KEY (role_id, permission_id),
    INDEX idx_role_permissions_role_id (role_id),
    INDEX idx_role_permissions_permission_id (permission_id)
);

User-Permissions Assignment Table

CREATE TABLE user_permissions (
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    PRIMARY KEY (user_id, permission_id),
    INDEX idx_user_permissions_user_id (user_id),
    INDEX idx_user_permissions_permission_id (permission_id)
);

User-Resource Attribute Assignment Table

CREATE TABLE user_resource_attributes (
    user_permission_id UUID REFERENCES user_permissions(id) ON DELETE CASCADE,
    resource_id UUID REFERENCES resources(id) ON DELETE CASCADE,
    attribute_value TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    PRIMARY KEY (user_permission_id, resource_id),
    INDEX idx_user_resource_attributes_user_permission_id (user_permission_id),
    INDEX idx_user_resource_attributes_resource_id (resource_id)
);

Role-Attribute Assignment Table

CREATE TABLE role_attributes (
    role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
    attribute_id UUID REFERENCES attributes(id) ON DELETE CASCADE,
    value TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    PRIMARY KEY (role_id, attribute_id),
    INDEX idx_role_attributes_role_id (role_id),
    INDEX idx_role_attributes_attribute_id (attribute_id)
);

User-Attribute Assignment Table

CREATE TABLE user_attributes (
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    attribute_id UUID REFERENCES attributes(id) ON DELETE CASCADE,
    value TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    PRIMARY KEY (user_id, attribute_id),
    INDEX idx_user_attributes_user_id (user_id),
    INDEX idx_user_attributes_attribute_id (attribute_id)
);

User Sessions Table

CREATE TABLE user_sessions (
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    session_id VARCHAR(255) PRIMARY KEY,
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_user_sessions_user_id (user_id),
    INDEX idx_user_sessions_expires_at (expires_at)
);

Primary Key Strategy

UUID is selected as the primary key strategy for all tables due to:

  • Global uniqueness across distributed systems
  • No need for coordination between services when generating IDs
  • Better performance in distributed environments compared to auto-increment
  • Security benefits by preventing enumeration attacks

Index Design

TableIndex NameFieldsTypeJustification
usersidx_users_emailemailSingleFor fast user lookups by email
usersidx_users_statusstatusSingleFor filtering active/inactive users
rolesidx_roles_namenameSingleFor fast role lookups by name
permissionsidx_permissions_resource_actionresource, actionCompositeFor efficient permission checks by resource and action
permissionsidx_permissions_namenameSingleFor fast permission lookups by name
resourcesidx_resources_namenameSingleFor fast resource lookups
resourcesidx_resources_typetypeSingleFor filtering by resource type
attributesidx_attributes_namenameSingleFor fast attribute lookups
user_rolesidx_user_roles_user_iduser_idSingleFor fast role lookups by user
user_rolesidx_user_roles_role_idrole_idSingleFor fast user lookups by role
role_permissionsidx_role_permissions_role_idrole_idSingleFor fast permission lookups by role
role_permissionsidx_role_permissions_permission_idpermission_idSingleFor fast role lookups by permission
user_permissionsidx_user_permissions_user_iduser_idSingleFor fast permission lookups by user
user_permissionsidx_user_permissions_permission_idpermission_idSingleFor fast user lookups by permission
user_resource_attributesidx_user_resource_attributes_user_permission_iduser_permission_idSingleFor fast resource attribute lookups
user_resource_attributesidx_user_resource_attributes_resource_idresource_idSingleFor fast permission lookups by resource
role_attributesidx_role_attributes_role_idrole_idSingleFor fast attribute lookups by role
role_attributesidx_role_attributes_attribute_idattribute_idSingleFor fast role lookups by attribute
user_attributesidx_user_attributes_user_iduser_idSingleFor fast attribute lookups by user
user_attributesidx_user_attributes_attribute_idattribute_idSingleFor fast user lookups by attribute
user_sessionsidx_user_sessions_user_iduser_idSingleFor fast session lookups by user
user_sessionsidx_user_sessions_expires_atexpires_atSingleFor efficient session cleanup

Data Partitioning / Sharding Strategy

Partitioning will be implemented using sharding by user ID:

  • Hash-based sharding on the user ID to distribute users evenly across shards
  • Each shard will contain a subset of users, their roles, permissions, and attributes
  • Cross-shard queries will be handled via a centralized authorization gateway that routes requests to appropriate shards

Soft Delete vs. Hard Delete Decision

Soft delete is chosen for the following reasons:

  • Auditability: Historical access patterns and role assignments need to be preserved
  • Recovery capability: In case of accidental deletions, data can be recovered
  • Compliance: Regulatory requirements often mandate retention of audit trails
  • Data integrity: Prevents cascading deletion issues in complex role hierarchies

Audit Trail Design

All data modification operations will be logged in an audit trail table:

CREATE TABLE audit_trail (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    operation VARCHAR(20) NOT NULL, -- CREATE, UPDATE, DELETE
    table_name VARCHAR(100) NOT NULL,
    record_id UUID NOT NULL,
    user_id UUID REFERENCES users(id),
    changes JSONB,
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_audit_trail_timestamp (timestamp),
    INDEX idx_audit_trail_user_id (user_id),
    INDEX idx_audit_trail_table_name (table_name)
);

Data Archival Strategy and Retention Policy

  • Active data: Users, roles, permissions, assignments remain in primary storage
  • Audit logs: Retained for 7 years (compliance requirement)
  • Session data: Retained for 30 days (security and analytics)
  • Archival process: Daily batch job to move audit logs older than 7 years to cold storage (e.g., AWS Glacier)
  • Index maintenance: Periodic reindexing of archived tables to maintain query performance

4. Class Design & Service Layer

classDiagram
    class AuthorizationService {
        +authorize(user, resource, action) boolean
        +hasPermission(user, resource, action) boolean
        +getPermissions(user, resource) Set~Permission~
        +assignRole(user, role) void
        +revokeRole(user, role) void
        +assignPermission(role, permission) void
        +revokePermission(role, permission) void
    }
    
    class RBACAuthorizationService {
        +authorize(user, resource, action) boolean
        +hasPermission(user, resource, action) boolean
        +getPermissions(user, resource) Set~Permission~
    }
    
    class ABACAuthorizationService {
        +authorize(user, resource, action) boolean
        +hasPermission(user, resource, action) boolean
        +getPermissions(user, resource) Set~Permission~
    }
    
    class Permission {
        -id: UUID
        -name: String
        -description: String
        -resource: String
        -action: String
        +equals() boolean
        +hashCode() int
    }
    
    class Role {
        -id: UUID
        -name: String
        -description: String
        -permissions: Set~Permission~
        +equals() boolean
        +hashCode() int
    }
    
    class User {
        -id: UUID
        -username: String
        -email: String
        -roles: Set~Role~
        +equals() boolean
        +hashCode() int
    }
    
    class Resource {
        -id: UUID
        -name: String
        -type: String
        -attributes: Map~String, Object~
        +equals() boolean
        +hashCode() int
    }
    
    class Policy {
        -id: UUID
        -name: String
        -description: String
        -conditions: List~Condition~
        -effect: Effect
    }
    
    class Condition {
        -attribute: String
        -operator: Operator
        -value: Object
    }
    
    class AuthorizationContext {
        -user: User
        -resource: Resource
        -action: String
        -requestTime: Instant
    }
    
    class PolicyEvaluator {
        +evaluate(policy, context) boolean
    }
    
    class PermissionResolver {
        +resolvePermissions(user, resource) Set~Permission~
    }
    
    AuthorizationService <|-- RBACAuthorizationService
    AuthorizationService <|-- ABACAuthorizationService
    
    RBACAuthorizationService --> User : "user"
    RBACAuthorizationService --> Role : "roles"
    RBACAuthorizationService --> Permission : "permissions"
    
    ABACAuthorizationService --> User : "user"
    ABACAuthorizationService --> Resource : "resource"
    ABACAuthorizationService --> Policy : "policies"
    
    PermissionResolver --> User : "user"
    PermissionResolver --> Role : "roles"
    PermissionResolver --> Permission : "permissions"
    
    PolicyEvaluator --> Policy : "policy"
    PolicyEvaluator --> AuthorizationContext : "context"
stateDiagram-v2
    [*] --> Idle
    Idle --> CheckingPermission : checkPermission()
    CheckingPermission --> Granted : Permission granted
    CheckingPermission --> Denied : Permission denied
    Granted --> [*]
    Denied --> [*]

Service Class Structure and Responsibilities

The authorization service follows a layered architecture with clear separation of concerns:

  1. AuthorizationService - Main interface defining authorization operations
  2. RBACAuthorizationService - Implements Role-Based Access Control logic
  3. ABACAuthorizationService - Implements Attribute-Based Access Control logic
  4. PermissionResolver - Resolves effective permissions for a user/resource combination
  5. PolicyEvaluator - Evaluates ABAC policies against authorization context

Interface and Abstract Class Design

public interface AuthorizationService {
    boolean authorize(User user, Resource resource, String action);
    boolean hasPermission(User user, Resource resource, String action);
    Set<Permission> getPermissions(User user, Resource resource);
    void assignRole(User user, Role role);
    void revokeRole(User user, Role role);
    void assignPermission(Role role, Permission permission);
    void revokePermission(Role role, Permission permission);
}

public abstract class AbstractAuthorizationService implements AuthorizationService {
    protected final PermissionResolver permissionResolver;
    protected final PolicyEvaluator policyEvaluator;
    
    public AbstractAuthorizationService(PermissionResolver resolver, PolicyEvaluator evaluator) {
        this.permissionResolver = resolver;
        this.policyEvaluator = evaluator;
    }
    
    @Override
    public boolean hasPermission(User user, Resource resource, String action) {
        return getPermissions(user, resource).stream()
            .anyMatch(p -> p.getAction().equals(action));
    }
}

Business Logic Encapsulation Patterns

Strategy Pattern Implementation:

public interface AuthorizationStrategy {
    boolean authorize(AuthorizationContext context);
}

public class RBACAuthorizationStrategy implements AuthorizationStrategy {
    private final PermissionResolver permissionResolver;
    
    public RBACAuthorizationStrategy(PermissionResolver resolver) {
        this.permissionResolver = resolver;
    }
    
    @Override
    public boolean authorize(AuthorizationContext context) {
        Set<Permission> permissions = permissionResolver.resolvePermissions(
            context.getUser(), 
            context.getResource()
        );
        
        return permissions.stream()
            .anyMatch(p -> p.getAction().equals(context.getAction()));
    }
}

public class ABACAuthorizationStrategy implements AuthorizationStrategy {
    private final PolicyEvaluator policyEvaluator;
    
    public ABACAuthorizationStrategy(PolicyEvaluator evaluator) {
        this.policyEvaluator = evaluator;
    }
    
    @Override
    public boolean authorize(AuthorizationContext context) {
        // Evaluate all applicable policies for the resource and action
        return policyEvaluator.evaluateAll(context);
    }
}

Critical-Path Method Implementation

public class RBACAuthorizationService extends AbstractAuthorizationService {
    private final UserRepository userRepository;
    private final RoleRepository roleRepository;
    private final PermissionRepository permissionRepository;
    private final EventPublisher eventPublisher;
    private final TransactionManager transactionManager;

    public RBACAuthorizationService(
            UserRepository userRepository,
            RoleRepository roleRepository,
            PermissionRepository permissionRepository,
            EventPublisher eventPublisher,
            TransactionManager transactionManager) {
        super(new PermissionResolverImpl(roleRepository, permissionRepository), 
               new PolicyEvaluatorImpl());
        this.userRepository = userRepository;
        this.roleRepository = roleRepository;
        this.permissionRepository = permissionRepository;
        this.eventPublisher = eventPublisher;
        this.transactionManager = transactionManager;
    }

    public boolean authorize(User user, Resource resource, String action) {
        // Transaction boundary - all operations in single transaction
        return transactionManager.executeInTransaction(() -> {
            // Validate inputs
            if (user == null || resource == null || action == null) {
                throw new IllegalArgumentException("User, resource and action must not be null");
            }

            // Fetch user with roles and permissions (optimized eager load)
            User userWithRoles = userRepository.findByIdWithRoles(user.getId());
            if (userWithRoles == null) {
                return false;
            }

            // Check direct permissions first (fast path)
            Set<Permission> effectivePermissions = permissionResolver.resolvePermissions(
                userWithRoles, resource);

            boolean hasPermission = effectivePermissions.stream()
                .anyMatch(p -> p.getAction().equals(action));

            // Log authorization decision
            AuthorizationEvent event = new AuthorizationEvent(
                userWithRoles.getId(),
                resource.getId(),
                action,
                hasPermission,
                Instant.now()
            );
            
            eventPublisher.publish(event);

            return hasPermission;
        });
    }

    public void assignRole(User user, Role role) {
        transactionManager.executeInTransaction(() -> {
            // Validate that user and role exist
            User existingUser = userRepository.findById(user.getId());
            Role existingRole = roleRepository.findById(role.getId());
            
            if (existingUser == null || existingRole == null) {
                throw new ResourceNotFoundException("User or Role not found");
            }

            // Add role to user (with locking to prevent race conditions)
            User userWithLock = userRepository.lockById(user.getId());
            userWithLock.addRole(existingRole);
            
            // Update in database
            userRepository.save(userWithLock);
            
            // Publish role assignment event for audit trail
            RoleAssignmentEvent event = new RoleAssignmentEvent(
                userWithLock.getId(),
                existingRole.getId(),
                Instant.now()
            );
            
            eventPublisher.publish(event);
        });
    }
}

Transaction Boundary Design

All authorization operations that modify state are wrapped in single database transactions to ensure atomicity:

  1. Role Assignment: User update + event publishing in single transaction
  2. Permission Check: Read operations with consistent snapshot isolation
  3. Policy Evaluation: Read-only operations that don't require transaction boundaries

Service Interface Contracts

public interface AuthorizationService {
    /**
     * Main authorization check - returns true if user has permission for resource/action
     */
    boolean authorize(User user, Resource resource, String action);
    
    /**
     * Fast path check - returns true if user has specific permission
     */
    boolean hasPermission(User user, Resource resource, String action);
    
    /**
     * Returns all effective permissions for a user/resource combination
     */
    Set<Permission> getPermissions(User user, Resource resource);
    
    /**
     * Assigns a role to a user - requires admin privileges
     */
    void assignRole(User user, Role role);
    
    /**
     * Revokes a role from a user - requires admin privileges
     */
    void revokeRole(User user, Role role);
    
    /**
     * Assigns a permission to a role - requires admin privileges
     */
    void assignPermission(Role role, Permission permission);
    
    /**
     * Revokes a permission from a role - requires admin privileges
     */
    void revokePermission(Role role, Permission permission);
}

Saga / Orchestration Pattern for Distributed Transactions

For distributed authorization systems, we implement a saga pattern:

public class AuthorizationSaga {
    private final List<SagaStep> steps;
    
    public void executeAuthorizationSaga(AuthorizationRequest request) {
        try {
            for (SagaStep step : steps) {
                step.execute();
            }
        } catch (Exception e) {
            // Compensate all previous steps
            compensateSteps();
            throw new AuthorizationException("Authorization failed", e);
        }
    }
    
    private void compensateSteps() {
        // Reverse the order of executed steps and call compensate()
        for (int i = steps.size() - 1; i >= 0; i--) {
            steps.get(i).compensate();
        }
    }
}

Domain Object / Value Object Design

public class User {
    private final UUID id;
    private final String username;
    private final String email;
    private final Set<Role> roles;
    
    // Constructor, getters, equals, hashCode
}

public class Role {
    private final UUID id;
    private final String name;
    private final String description;
    private final Set<Permission> permissions;
    
    // Constructor, getters, equals, hashCode
}

public class Permission {
    private final UUID id;
    private final String name;
    private final String description;
    private final String resource;
    private final String action;
    
    // Constructor, getters, equals, hashCode
}

public class Resource {
    private final UUID id;
    private final String name;
    private final String type;
    private final Map<String, Object> attributes;
    
    // Constructor, getters, equals, hashCode
}

public class AuthorizationContext {
    private final User user;
    private final Resource resource;
    private final String action;
    private final Instant requestTime;
    
    // Constructor, getters
}

5. Concurrency & Thread Safety

Concurrency Model

The authorization service will use a thread-safe, single-threaded event loop model for processing requests. This approach is chosen to minimize contention and simplify state management, particularly for concurrent access to permission checks. Each request will be handled by a dedicated thread from a fixed-size thread pool, ensuring predictable resource usage and avoiding thread starvation.

Race Condition Prevention Strategy

Race conditions in authorization checks will be prevented through:

  1. Immutable data structures - All permission and policy objects are immutable once created
  2. Read-locks on shared resources - When checking policies, read locks are acquired to prevent concurrent modifications
  3. Atomic operations for counters and metrics - Using atomic references for tracking authorization decisions

Optimistic vs. Pessimistic Locking Selection

Optimistic locking is selected for the authorization service because:

  • Authorization checks are read-heavy operations with minimal write contention
  • The system prioritizes throughput over strict consistency in permission evaluation
  • Policy updates are infrequent and can be handled with conflict resolution
  • The risk of concurrent policy modification is low since policies are typically updated by admin users in batches

Deadlock Prevention Strategy

Deadlock prevention is implemented through:

  1. Fixed ordering of resource acquisition - All lock acquisitions follow a consistent order (e.g., by policy ID, then role ID)
  2. Timeout-based lock acquisition - Locks are acquired with a 50ms timeout to prevent indefinite blocking
  3. Hierarchical lock management - Locks are organized in a hierarchy to prevent circular dependencies

Idempotency Implementation for Concurrent Duplicate Requests

Idempotency is handled at the service layer by:

  1. Request deduplication using a cache - Requests with identical parameters are cached with TTL of 10 seconds
  2. Idempotency key validation - Clients can provide an idempotency key in the request header that is used to identify and deduplicate requests
  3. Atomic operation boundaries - Authorization decisions are wrapped in atomic operations to ensure consistent state
public class AuthorizationService {
    private final Map<String, AuthorizationResult> requestCache = new ConcurrentHashMap<>();
    private final ReadWriteLock policyLock = new ReentrantReadWriteLock();
    
    public AuthorizationResult checkPermission(
            String userId, 
            String resource, 
            String action,
            String idempotencyKey) {
        
        // Idempotency check
        if (idempotencyKey != null) {
            AuthorizationResult cached = requestCache.get(idempotencyKey);
            if (cached != null) {
                return cached;
            }
        }
        
        // Acquire read lock for policy access
        policyLock.readLock().lock();
        try {
            // Perform authorization check
            AuthorizationResult result = performAuthorizationCheck(userId, resource, action);
            
            // Cache result for idempotency
            if (idempotencyKey != null) {
                requestCache.put(idempotencyKey, result);
                // Remove from cache after TTL
                ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
                scheduler.schedule(() -> requestCache.remove(idempotencyKey), 10, TimeUnit.SECONDS);
            }
            
            return result;
        } finally {
            policyLock.readLock().unlock();
        }
    }
    
    private AuthorizationResult performAuthorizationCheck(String userId, String resource, String action) {
        // Implementation details for policy evaluation
        return new AuthorizationResult(true); // Simplified for example
    }
}

6. Sequence Diagrams for Key Flows

Happy Path Sequence Diagram

sequenceDiagram
    participant Client
    participant AuthzService
    participant AuthService
    participant DB

    Client->>AuthzService: Request with JWT token
    AuthzService->>AuthService: Validate token
    AuthService-->>AuthzService: Token valid, user_id=123
    AuthzService->>DB: Fetch user roles and permissions
    DB-->>AuthzService: Roles: [admin, editor], Permissions: [read, write]
    AuthzService->>DB: Fetch resource policies (ABAC)
    DB-->>AuthzService: Policy: user.department == resource.department
    AuthzService->>DB: Fetch resource details (if needed)
    DB-->>AuthzService: Resource: {department: "engineering"}
    AuthzService->>AuthzService: Evaluate RBAC + ABAC rules
    AuthzService-->>Client: 200 OK / 403 Forbidden

Error / Edge Case Sequence Diagram

sequenceDiagram
    participant Client
    participant AuthzService
    participant AuthService
    participant DB

    Client->>AuthzService: Request with invalid token
    AuthzService->>AuthService: Validate token
    AuthService-->>AuthzService: Token invalid, throw exception
    AuthzService-->>Client: 401 Unauthorized

    Client->>AuthzService: Request with valid token but no permissions
    AuthzService->>AuthService: Validate token
    AuthService-->>AuthzService: Token valid, user_id=123
    AuthzService->>DB: Fetch user roles and permissions
    DB-->>AuthzService: Roles: [viewer], Permissions: [read]
    AuthzService->>DB: Fetch resource policies (ABAC)
    DB-->>AuthzService: Policy: user.department == resource.department
    AuthzService->>DB: Fetch resource details (if needed)
    DB-->>AuthzService: Resource: {department: "marketing"}
    AuthzService->>AuthzService: Evaluate RBAC + ABAC rules
    AuthzService-->>Client: 403 Forbidden

    Client->>AuthzService: Request with expired token
    AuthzService->>AuthService: Validate token
    AuthService-->>AuthzService: Token expired, throw exception
    AuthzService-->>Client: 401 Unauthorized

Async Flow Sequence Diagram

sequenceDiagram
    participant Client
    participant AuthzService
    participant EventPublisher
    participant EventConsumer
    participant DB

    Client->>AuthzService: Create user request
    AuthzService->>DB: Create user in DB
    DB-->>AuthzService: User created
    AuthzService->>EventPublisher: Publish user.created event
    EventPublisher-->>AuthzService: Event published
    AuthzService->>EventConsumer: Handle user.created event (assign roles)
    EventConsumer->>DB: Assign default roles to user
    DB-->>EventConsumer: Roles assigned
    AuthzService-->>Client: 201 Created

7. Caching Design

Cache Strategy Selection

For the authorization service, we'll use a cache-aside pattern with LRU eviction policy. This approach provides the best balance between performance and consistency for authorization decisions, where freshness is more important than absolute cache hit rates.

Cache Key Design

Cache keys will follow a structured namespace pattern:

auth:{resource_type}:{resource_id}:{user_id}:{permission_name}

Example key for checking if user 123 has "read" permission on resource 456:

auth:document:456:123:read

TTL Strategy

Data TypeTTLReason
User permissions5 minutesFrequent updates during role changes
Role assignments10 minutesLess frequent than permissions
Permission trees1 hourStable and expensive to compute
Policy evaluations30 secondsHigh cardinality, low volatility

Cache Eviction Policy

LRU (Least Recently Used) with probabilistic early expiry:

  • Primary eviction: LRU-based
  • Secondary: 10% probabilistic early expiry to prevent cache stampede
  • Cache size: 100K entries per node

Cache Stampede Prevention

Implement probabilistic early expiry with 10% chance of expiring entries 2 minutes before TTL:

public class CacheStampedePrevention {
    private static final double EXPIRY_PROBABILITY = 0.1;
    
    public boolean shouldExpireEarly() {
        return Math.random() < EXPIRY_PROBABILITY;
    }
}

Distributed Cache Architecture

Using Redis Cluster with:

  • 3 master nodes for high availability
  • 3 replica nodes per master for read scaling
  • Redis Sentinel for automatic failover
  • Cache sharding by user ID hash to ensure even distribution

Cache Invalidation Strategy

Event-driven invalidation with explicit purging:

  1. Immediate invalidation: On role/permission changes
  2. Batch invalidation: Periodic cleanup of stale entries (every 5 minutes)
  3. Graceful degradation: Return cached value with warning header if cache is unavailable
@EventListener
public void handleRoleAssignmentChanged(RoleAssignmentChangedEvent event) {
    String cacheKey = buildCacheKey(event.getResourceType(), 
                                   event.getResourceId(), 
                                   event.getUserId(), 
                                   event.getPermission());
    cacheManager.evict(cacheKey);
}

Cache Read-Through Pattern

public boolean hasPermission(String userId, String resourceType, 
                           String resourceId, String permission) {
    String cacheKey = buildCacheKey(resourceType, resourceId, userId, permission);
    
    Boolean cachedResult = cache.get(cacheKey);
    if (cachedResult != null) {
        return cachedResult;
    }
    
    // Cache miss - compute and store
    boolean result = computePermission(userId, resourceType, resourceId, permission);
    cache.put(cacheKey, result, TTL_FOR_PERMISSIONS);
    
    return result;
}

8. Repository / Data Access Layer

Repository Pattern Implementation

The authorization 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 and specialized queries.

public interface UserRepository {
    Optional<User> findById(UUID userId);
    List<User> findAll();
    User save(User user);
    void delete(UUID userId);
    List<User> findByRole(String role);
    boolean existsById(UUID userId);
}

public interface RoleRepository {
    Optional<Role> findById(UUID roleId);
    List<Role> findAll();
    Role save(Role role);
    void delete(UUID roleId);
    List<Role> findByUser(UUID userId);
    Set<String> findRoleNamesByUser(UUID userId);
}

public interface PermissionRepository {
    Optional<Permission> findById(UUID permissionId);
    List<Permission> findAll();
    Permission save(Permission permission);
    void delete(UUID permissionId);
    Set<String> findPermissionsByRole(UUID roleId);
}

public interface UserPermissionRepository {
    void assignPermissionToUser(UUID userId, UUID permissionId);
    void revokePermissionFromUser(UUID userId, UUID permissionId);
    Set<UUID> findPermissionsByUser(UUID userId);
    boolean hasPermission(UUID userId, String permissionName);
}

public interface RolePermissionRepository {
    void assignPermissionToRole(UUID roleId, UUID permissionId);
    void revokePermissionFromRole(UUID roleId, UUID permissionId);
    Set<UUID> findPermissionsByRole(UUID roleId);
    boolean hasPermission(UUID roleId, String permissionName);
}

ORM vs Raw Query Decisions

For the authorization service, we use JPA/Hibernate for most entities due to its maturity and ability to handle complex relationships, but we use raw SQL for performance-critical permission checks that involve large datasets.

@Repository
public class PermissionRepositoryImpl implements UserPermissionRepository {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public boolean hasPermission(UUID userId, String permissionName) {
        // Raw SQL for performance - avoids object mapping overhead
        String sql = """
            SELECT EXISTS (
                SELECT 1 FROM user_permissions up
                JOIN permissions p ON up.permission_id = p.id
                WHERE up.user_id = :userId AND p.name = :permissionName
            )
        """;
        
        Query query = entityManager.createNativeQuery(sql);
        query.setParameter("userId", userId);
        query.setParameter("permissionName", permissionName);
        
        return ((Number) query.getSingleResult()).intValue() == 1;
    }
    
    // Other methods...
}

Read vs Write Separation

We implement CQRS pattern for the authorization service to separate read and write operations:

// Write side - for mutations (roles, permissions, assignments)
@Repository
public class AuthorizationWriteRepository {
    @PersistenceContext
    private EntityManager entityManager;
    
    public void saveUser(User user) {
        entityManager.persist(user);
    }
    
    public void assignRoleToUser(UUID userId, UUID roleId) {
        // Implementation for role assignment
    }
    
    public void assignPermissionToRole(UUID roleId, UUID permissionId) {
        // Implementation for permission assignment
    }
}

// Read side - optimized for queries (user permissions, role hierarchies)
@Repository
public class AuthorizationReadRepository {
    @PersistenceContext
    private EntityManager entityManager;
    
    public Set<String> getUserPermissions(UUID userId) {
        // Optimized query for fetching user permissions
        String sql = """
            SELECT p.name FROM permissions p
            JOIN user_permissions up ON p.id = up.permission_id
            WHERE up.user_id = :userId
        """;
        
        Query query = entityManager.createNativeQuery(sql);
        query.setParameter("userId", userId);
        
        return query.getResultList().stream()
            .map(String.class::cast)
            .collect(Collectors.toSet());
    }
    
    public Set<String> getUserRoles(UUID userId) {
        // Optimized query for fetching user roles
        String sql = """
            SELECT r.name FROM roles r
            JOIN user_roles ur ON r.id = ur.role_id
            WHERE ur.user_id = :userId
        """;
        
        Query query = entityManager.createNativeQuery(sql);
        query.setParameter("userId", userId);
        
        return query.getResultList().stream()
            .map(String.class::cast)
            .collect(Collectors.toSet());
    }
}

Connection Pool Configuration

We configure connection pooling to handle concurrent authorization requests efficiently:

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

N+1 Query Prevention Strategy

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

@Entity
@Table(name = "users")
@NamedEntityGraph(
    name = "User.withRolesAndPermissions",
    attributeNodes = @NamedAttributeNode(value = "roles", subgraph = "roles-subgraph"),
    subgraphs = @NamedSubgraph(
        name = "roles-subgraph",
        attributeNodes = @NamedAttributeNode(value = "permissions", subgraph = "permissions-subgraph"),
        subgraphs = @NamedSubgraph(
            name = "permissions-subgraph",
            attributeNodes = @NamedAttributeNode("name")
        )
    )
)
public class User {
    @Id
    private UUID id;
    
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
        name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();
    
    // Other fields...
}

// Repository method that uses fetch joins to prevent N+1
@Repository
public class UserRepositoryImpl {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public User findUserWithRolesAndPermissions(UUID userId) {
        String jpql = """
            SELECT u FROM User u 
            LEFT JOIN FETCH u.roles r 
            LEFT JOIN FETCH r.permissions p
            WHERE u.id = :userId
        """;
        
        TypedQuery<User> query = entityManager.createQuery(jpql, User.class);
        query.setParameter("userId", userId);
        
        return query.getSingleResult();
    }
}

Batch Operation Design

For bulk operations like role assignment, we implement batch processing:

@Repository
public class BatchRoleAssignmentRepository {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public void assignRolesToUsers(List<UUID> userIds, List<UUID> roleIds) {
        // Batch insert for role assignments
        String sql = """
            INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)
        """;
        
        try (Connection connection = entityManager.unwrap(Session.class).connection()) {
            PreparedStatement stmt = connection.prepareStatement(sql);
            
            for (UUID userId : userIds) {
                for (UUID roleId : roleIds) {
                    stmt.setObject(1, userId);
                    stmt.setObject(2, roleId);
                    stmt.addBatch();
                }
            }
            
            stmt.executeBatch();
        } catch (SQLException e) {
            throw new DataAccessException("Failed to batch assign roles", e);
        }
    }
}

Transaction Management

All repository operations are properly wrapped in transaction boundaries:

@Service
@Transactional
public class AuthorizationService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private RoleRepository roleRepository;
    
    @Autowired
    private UserPermissionRepository userPermissionRepository;
    
    public void assignRoleToUser(UUID userId, UUID roleId) {
        // Transaction boundary ensures consistency
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException(userId));
        
        Role role = roleRepository.findById(roleId)
            .orElseThrow(() -> new RoleNotFoundException(roleId));
        
        // This operation is atomic within the transaction
        user.getRoles().add(role);
        userRepository.save(user);
    }
}

Query Optimization

We implement query optimization strategies including:

  1. Composite indexes on frequently queried columns
  2. Covering indexes for read-heavy operations
  3. Partitioned tables for large datasets
-- Indexes for authorization queries
CREATE INDEX idx_user_permissions_user_id ON user_permissions(user_id);
CREATE INDEX idx_user_permissions_permission_id ON user_permissions(permission_id);
CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
CREATE INDEX idx_user_roles_role_id ON user_roles(role_id);

-- Covering index for permission lookup
CREATE INDEX idx_permissions_covering 
ON permissions(name, id) INCLUDE (description);

The repository layer is designed for high performance under concurrent load while maintaining data consistency and providing clean separation between domain logic and data access concerns.

9. Asynchronous Processing & Messaging

Event-driven boundaries in this feature

The authorization service is primarily synchronous for real-time access control decisions, but certain operations benefit from asynchronous processing:

  1. Policy change propagation - When a policy is updated, we need to invalidate caches and notify dependent services asynchronously
  2. Audit logging - Authorization decisions are logged for compliance and monitoring, which can be done asynchronously to avoid impacting decision latency
  3. User role assignment updates - When a user's roles change, we may want to notify downstream systems for cache invalidation or re-authentication

The core authorization decision flow (checkPermission) remains synchronous to ensure low-latency response for access control checks.

Message schema design

{
  "version": "1.0",
  "event_type": "policy.changed",
  "timestamp": "2023-10-15T14:30:00Z",
  "source": "auth-service",
  "payload": {
    "policy_id": "policy-123",
    "policy_name": "read-user-data",
    "updated_by": "admin@company.com",
    "changes": {
      "effect": "allow",
      "conditions": [
        {
          "attribute": "user.department",
          "operator": "equals",
          "value": "engineering"
        }
      ]
    }
  },
  "correlation_id": "req-abc-123"
}

Dead letter queue strategy and poison message handling

Poison messages are handled through a dead-letter queue (DLQ) pattern:

  • Messages that fail more than 3 times are moved to a DLQ topic
  • DLQ messages are logged with full context for manual inspection
  • A separate monitoring dashboard alerts on DLQ message volume
  • Failed messages are retried with exponential backoff (1s, 5s, 10s) before moving to DLQ

At-least-once vs. exactly-once delivery

At-least-once delivery is chosen for authorization events because:

  • Authorization decisions are idempotent - reprocessing a policy change event won't cause harm
  • The system can handle duplicate events gracefully through cache invalidation and state reconciliation
  • Implementation complexity is reduced compared to exactly-once semantics

Retry policy for consumers

public class AuthorizationEventConsumer {
    private static final int MAX_RETRIES = 3;
    private static final long INITIAL_DELAY_MS = 1000;
    private static final long MAX_DELAY_MS = 30000;
    
    public void processEvent(AuthorizationEvent event) {
        int attempts = 0;
        while (attempts < MAX_RETRIES) {
            try {
                handleEvent(event);
                return;
            } catch (Exception e) {
                attempts++;
                if (attempts >= MAX_RETRIES) {
                    moveMessageToDLQ(event, e);
                    break;
                }
                
                long delay = Math.min(INITIAL_DELAY_MS * (1L << attempts), MAX_DELAY_MS);
                Thread.sleep(delay + randomJitter(delay));
            }
        }
    }
    
    private long randomJitter(long baseDelay) {
        return (long) (Math.random() * baseDelay * 0.1);
    }
}

Outbox pattern for reliable event publishing

The outbox pattern is implemented for authorization events to ensure eventual consistency:

@Entity
@Table(name = "authorization_events_outbox")
public class AuthorizationEventOutbox {
    @Id
    private UUID id;
    
    @Column(name = "event_type")
    private String eventType;
    
    @Column(name = "payload")
    private String payload;
    
    @Column(name = "status")
    private String status; // PENDING, PROCESSED, FAILED
    
    @Column(name = "created_at")
    private Instant createdAt;
    
    @Column(name = "processed_at")
    private Instant processedAt;
}

Sequence diagram for async flows

sequenceDiagram
    participant A as Auth Service
    participant O as Outbox Table
    participant M as Message Broker
    participant C as Cache Service
    participant L as Audit Logger
    
    A->>O: Insert event (policy.changed)
    O-->>A: Acknowledge
    A->>M: Publish to policy-changes topic
    M-->>A: Acknowledge
    M->>C: Notify policy change
    C-->>M: Acknowledge
    M->>L: Log audit event
    L-->>M: Acknowledge

10. Error Handling Design

Error Classification Hierarchy

The authorization service implements a three-tier error classification system:

public enum ErrorType {
    VALIDATION_ERROR,        // Input validation failures
    AUTHORIZATION_ERROR,     // Permission denied
    INFRASTRUCTURE_ERROR,    // Database, cache, or external service failures
    DOMAIN_ERROR,            // Business logic violations (e.g., invalid role)
    SYSTEM_ERROR             // Unexpected internal failures
}

Error Propagation Strategy

  • Validation errors: Caught at API boundary and converted to 400 responses
  • Authorization errors: Translated to 403 responses at service boundary
  • Infrastructure errors: Bubble up to centralized error handler with retry logic where appropriate
  • Domain errors: Handled at service layer and converted to appropriate HTTP status codes
  • System errors: Logged with full stack trace and returned as 500 responses

Retry Strategy

For transient infrastructure errors (e.g., database timeouts, cache unavailability):

public class RetryableOperation {
    private static final int MAX_RETRIES = 3;
    private static final long BASE_DELAY_MS = 100;
    
    public <T> T executeWithRetry(Supplier<T> operation) {
        int attempts = 0;
        while (true) {
            try {
                return operation.get();
            } catch (InfrastructureException e) {
                if (++attempts >= MAX_RETRIES) {
                    throw new SystemException("Operation failed after " + MAX_RETRIES + " attempts", e);
                }
                long delay = BASE_DELAY_MS * Math.pow(2, attempts) + 
                           ThreadLocalRandom.current().nextLong(100);
                Thread.sleep(delay);
            }
        }
    }
}

Circuit Breaker Design

public class CircuitBreaker {
    private final int failureThreshold = 5;
    private final long timeoutMs = 60000;
    private final int resetTimeoutMs = 30000;
    
    private volatile CircuitState state = CircuitState.CLOSED;
    private int failureCount = 0;
    private long lastFailureTime = 0;
    
    public <T> T execute(Supplier<T> operation) {
        switch (state) {
            case CLOSED:
                try {
                    T result = operation.get();
                    failureCount = 0;
                    return result;
                } catch (Exception e) {
                    failureCount++;
                    lastFailureTime = System.currentTimeMillis();
                    if (failureCount >= failureThreshold) {
                        state = CircuitState.OPEN;
                    }
                    throw e;
                }
            case OPEN:
                if (System.currentTimeMillis() - lastFailureTime > resetTimeoutMs) {
                    state = CircuitState.HALF_OPEN;
                } else {
                    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;
                }
        }
    }
}

Fallback Behavior Design

For critical authorization checks, fallback to a safe default:

public class AuthorizationFallback {
    private final boolean allowByDefault;
    
    public AuthorizationResult checkPermissionWithFallback(
            String userId, String resource, String action) {
        try {
            return authorizationService.checkPermission(userId, resource, action);
        } catch (Exception e) {
            // Log the failure for monitoring
            logger.warn("Authorization check failed for user {}, resource {}, action {}. Using fallback.", 
                       userId, resource, action, e);
            
            // Fallback to deny if configured as such
            return new AuthorizationResult(false, "Fallback: permission denied");
        }
    }
}

Error Logging Format

Structured logging with trace context:

public class AuthorizationErrorLogger {
    private final ObjectMapper objectMapper;
    
    public void logAuthorizationError(String traceId, String userId, 
                                    String resource, String action, Exception error) {
        Map<String, Object> logEntry = new HashMap<>();
        logEntry.put("timestamp", Instant.now().toString());
        logEntry.put("trace_id", traceId);
        logEntry.put("user_id", userId);
        logEntry.put("resource", resource);
        logEntry.put("action", action);
        logEntry.put("error_type", error.getClass().getSimpleName());
        logEntry.put("error_message", error.getMessage());
        logEntry.put("stack_trace", getSanitizedStackTrace(error));
        
        logger.error(objectMapper.writeValueAsString(logEntry));
    }
    
    private String getSanitizedStackTrace(Exception error) {
        // Remove sensitive data from stack trace
        return Arrays.stream(error.getStackTrace())
                .map(ste -> ste.getClassName() + ":" + ste.getMethodName() + ":" + ste.getLineNumber())
                .collect(Collectors.joining("\n"));
    }
}

Alerting Thresholds

Error TypeAlert ThresholdSeverity
AUTHORIZATION_ERROR100 errors/minuteWarning
INFRASTRUCTURE_ERROR50 errors/minuteCritical
SYSTEM_ERROR10 errors/minuteCritical

Alerts are sent to on-call engineers via PagerDuty with escalation policies for critical errors.

11. Security Design

Input Validation and Sanitization Strategy

All inputs are validated using a combination of:

  • Regex patterns for format validation (e.g., email, UUID)
  • Length limits to prevent buffer overflow attacks
  • Whitelist-based validation for allowed characters and values
  • Custom validators for complex business rules

For example, role names are validated with regex ^[a-zA-Z0-9_-]{1,64}$ to prevent injection and ensure proper naming.

SQL Injection Prevention

  • All database queries use parameterized prepared statements exclusively
  • No dynamic query building with user input
  • ORM is configured to prevent raw SQL injection (e.g., Hibernate with proper parameter binding)
  • Input sanitization is applied as an additional layer

Authentication Middleware Design

Authentication middleware intercepts all requests and:

  1. Extracts JWT from Authorization header
  2. Validates token signature using public key (RSA-2048)
  3. Verifies expiration and audience claims
  4. Extracts user ID and roles from token payload
  5. Stores authenticated context in request scope

Authorization Enforcement (RBAC/ABAC)

RBAC Implementation:

  • Role-based access control with hierarchical role inheritance
  • Users are assigned to roles, roles have permissions
  • Permissions are grouped into scopes (e.g., user:read, user:write)

ABAC Implementation:

  • Attribute-based access control for dynamic policies
  • Policies are evaluated using JEXL expression engine (e.g., user.department == resource.department)
  • Context-aware permissions with time-based conditions
// Example policy evaluation logic
public boolean evaluatePolicy(String policyExpression, Map<String, Object> context) {
    JexlEngine jexl = new JexlBuilder().create();
    JexlExpression expression = jexl.createExpression(policyExpression);
    return (Boolean) expression.evaluate(new MapContext(context));
}

Sensitive Data Encryption at Rest and in Transit

At Rest:

  • AES-256-GCM encryption for sensitive data fields (e.g., user passwords, tokens)
  • Key rotation every 90 days using AWS KMS or Vault
  • Data is encrypted before storage in database

In Transit:

  • TLS 1.3 enforced for all API endpoints (minimum)
  • All internal service-to-service communication uses mTLS
  • JWT tokens are signed with RS256 (RSA 2048-bit keys)

PII Handling and Masking Strategy

  • PII fields (user emails, phone numbers) are encrypted at rest
  • Masking in logs: Email addresses shown as user***@domain.com
  • Redaction in responses: Sensitive fields like passwords are never returned
  • Audit logging includes only necessary PII with proper masking

Secrets Management

  • Vault (HashiCorp) for secrets lifecycle management:
    • API keys, database passwords, JWT signing keys
    • Secrets rotated every 30 days with automatic renewal
  • Environment-based secrets:
    • Production secrets stored in AWS Secrets Manager or GCP Secret Manager
    • Development/staging use local Vault instances

Audit Logging Design

Events Logged:

  • User login/logout
  • Role assignment/revoke
  • Policy creation/update/delete
  • Permission grant/deny operations

Log Format:

{
  "timestamp": "2023-10-27T14:30:00Z",
  "trace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "user_id": "user-uuid-12345",
  "action": "ROLE_ASSIGNMENT",
  "resource_type": "user",
  "resource_id": "user-uuid-67890",
  "details": {
    "assigned_role": "admin",
    "requesting_user": "admin-uuid"
  },
  "ip_address": "10.0.0.1",
  "user_agent": "Mozilla/5.0..."
}

Retention:

  • Audit logs retained for 3 years
  • Logs are stored in secure, immutable storage (e.g., AWS S3 with versioning)
  • Compliance reporting generated monthly from audit logs

12. Testing Strategy

Unit Testing Scope and Coverage Targets

Unit tests will cover 85%+ of business logic and service layer code with 90%+ line coverage. Focus on:

  • Core authorization decision logic (RBAC and ABAC evaluation)
  • Policy rule parsing and validation
  • Permission assignment and revocation workflows
  • Role hierarchy traversal and inheritance resolution
  • Context attribute processing and matching logic

Integration Testing Strategy

Database Integration Tests:

  • Use testcontainers for PostgreSQL to ensure schema compatibility and query correctness
  • Test transaction boundaries and concurrent access scenarios
  • Validate data consistency across multi-table operations (roles, permissions, policies)

External Service Integration Tests:

  • Mock external auth providers (OAuth2, SAML) using WireMock or embedded servers
  • Test integration with audit logging services and metrics collectors
  • Validate event publishing to message brokers (e.g., Kafka/Redis) in integration tests

End-to-End Test Scenarios:

  • Complete authorization flow from request to decision with real policy evaluation
  • Test cascading role inheritance and attribute-based condition matching
  • Validate policy conflict resolution and precedence handling

Contract Testing

Implement contract testing using Pact for:

  • API boundary contracts between authorization service and client applications
  • Message format contracts with downstream consumers (e.g., API gateway, microservices)
  • Version compatibility testing for policy schema changes

Performance Testing Tool and Scenarios

Load Testing Framework: k6 (JavaScript-based, scalable for high-concurrency scenarios)

Key Performance Scenarios:

  1. Concurrent authorization requests (500-1000 concurrent users)
  2. Policy evaluation under high load (10,000+ policies in system)
  3. Complex ABAC attribute matching performance (50+ attributes per request)
  4. Cache hit vs. miss performance comparison

Target Metrics:

  • p50 latency < 10ms for 95% of requests
  • p95 latency < 50ms for 95% of requests
  • Throughput: 20,000+ req/sec for basic RBAC checks
  • Error rate < 0.1% under load

Test Data Management Strategy

Test Data Generation:

  • Use factory patterns to generate realistic test data for:
    • Users (1000+ with various role combinations)
    • Roles (hierarchical structures up to 5 levels deep)
    • Permissions (1000+ with different resource types and actions)
    • Policies (ABAC conditions, RBAC role assignments)

Fixtures and Seeding:

  • Pre-populate test database with common authorization scenarios
  • Use fixtures for edge case testing (circular role references, policy conflicts)
  • Implement data seeding scripts that can be run in CI/CD pipeline

Test Data Isolation:

  • Use database transaction rollback per test method to ensure clean state
  • Implement test-specific schema or database instances for parallel test execution
  • Separate test data from production data with distinct namespaces and prefixes
// Example unit test for policy evaluation logic
@Test
public void testPolicyEvaluationWithAttributeMatch() {
    // Given: User with specific attributes and policy with attribute conditions
    User user = createUserWithAttributes("department", "engineering", "location", "sf");
    Policy policy = createPolicyWithAttributeCondition("department", "engineering");
    
    // When: Authorization check is performed
    boolean result = authorizationService.canAccess(user, "read", "resource1");
    
    // Then: Should grant access based on matching attributes
    assertTrue(result);
}

13. Observability Design

Structured Logging Schema

All logs must follow a consistent schema to enable effective search, filtering, and analysis:

{
  "timestamp": "2023-10-27T14:30:45.123Z",
  "level": "INFO|WARN|ERROR",
  "service": "auth-service",
  "trace_id": "12345678-1234-1234-1234-123456789012",
  "span_id": "abcdef12-3456-7890-abcd-ef1234567890",
  "user_id": "user-123",
  "entity_id": "resource-456",
  "operation": "checkPermission",
  "request_id": "req-789",
  "message": "Permission check completed",
  "duration_ms": 45,
  "error_code": "PERMISSION_DENIED",
  "error_message": "User does not have required permission",
  "stack_trace": "..."
}

PII masking rules:

  • User IDs and emails are masked in logs when not required for debugging
  • Token values are truncated (first 4, last 4 characters shown)
  • Sensitive attribute values (e.g., passwords) are never logged

Distributed Tracing Instrumentation

Using OpenTelemetry for end-to-end tracing:

Custom spans to add:

  1. auth.check_permission - measures the entire permission check flow
  2. auth.evaluate_policy - for ABAC policy evaluation logic
  3. auth.fetch_user_roles - database fetch for user role mappings
  4. auth.validate_token - JWT validation and claims extraction

Attributes to capture per key operation:

Span currentSpan = Span.current();
currentSpan.setAttribute("user.id", userId);
currentSpan.setAttribute("resource.type", resourceType);
currentSpan.setAttribute("permission.action", action);
currentSpan.setAttribute("policy.evaluation.result", evaluationResult);

Metrics Design at Service Level

RED metrics for monitoring service health:

Metric NameTypeDescriptionLabels
auth_requests_totalCounterTotal authorization requestsmethod, status, user_id
auth_request_duration_secondsHistogramRequest processing timemethod, status
auth_errors_totalCounterAuthorization errorserror_type, user_id
auth_cache_hit_ratioGaugeCache hit ratecache_type
auth_active_sessionsGaugeCurrent active sessionsuser_id

Health Check Endpoint Design

GET /health
GET /ready
GET /live
GET /startup

/health - Comprehensive health check:

{
  "status": "healthy",
  "checks": {
    "database": {"status": "healthy", "duration_ms": 15},
    "cache": {"status": "healthy", "duration_ms": 8},
    "auth_service": {"status": "healthy", "duration_ms": 2}
  }
}

/ready - Service ready for traffic:

{
  "status": "ready",
  "database": "connected",
  "cache": "connected"
}

/live - Service alive:

{
  "status": "alive",
  "uptime_seconds": 3600,
  "version": "v1.2.3"
}

/startup - Startup completion check:

{
  "status": "completed",
  "components_initialized": ["database", "cache", "policy_engine"]
}

Alerting Thresholds per Key Metric

MetricWarning LevelCritical LevelEscalation Path
auth_request_duration_seconds (p99)> 500ms> 2sOn-call engineer
auth_errors_total (rate)> 10/minute> 100/minuteSRE team
auth_cache_hit_ratio< 70%< 50%SRE team
auth_requests_total (rate)> 10k/second> 50k/secondSRE team
auth_active_sessions (rate)> 10k> 50kSRE team

Alerts should include:

  • Service degradation notifications
  • Cache miss rate spikes
  • Authentication failure bursts
  • Database connection pool exhaustion

14. Open Questions, Risks & Trade-offs

Decisions requiring product / engineering alignment

  • Policy enforcement granularity: Should authorization checks be at the API endpoint level, resource level, or field level? This decision impacts performance vs. flexibility trade-offs.
  • Cross-service authorization: How do we handle authorization across microservices? Should each service independently validate permissions or rely on a centralized auth service?
  • User vs. Application permissions: Should the service support both user-based and application-based authorization models, or focus on one primary use case?

Known trade-offs and justifications

  • Performance vs. flexibility: Using ABAC for fine-grained authorization provides maximum flexibility but introduces performance overhead from policy evaluation. RBAC offers better performance with less flexibility.
  • Complexity vs. maintainability: ABAC introduces significant complexity in policy definition and evaluation, but provides better extensibility for evolving access requirements.
  • Centralized vs. distributed auth: Centralizing authorization logic provides consistency but creates a single point of failure and potential performance bottleneck.
  • Real-time vs. cached decisions: Real-time policy evaluation ensures accuracy but increases latency; caching improves performance at risk of stale permissions.

Risks and mitigation strategies

  • Policy evaluation performance: Complex ABAC policies can cause significant latency. Mitigation includes policy optimization, caching, and pre-computing common access patterns.
  • Permission drift: Authorization policies may become outdated as system evolves. Mitigation includes regular policy audits, automated compliance checks, and clear ownership of authorization rules.
  • Race conditions in concurrent access: Multiple simultaneous permission checks could lead to inconsistent results. Mitigation includes proper locking mechanisms and atomic operations.
  • Cache invalidation storms: When policies change, cache invalidation can cause performance degradation. Mitigation includes smart cache invalidation strategies and TTL-based approaches.

Technical debt acknowledged

  • Policy language complexity: The ABAC implementation uses a custom policy language that may require future refactoring to support more sophisticated rule engines like Open Policy Agent (OPA).
  • Audit logging overhead: Comprehensive audit logging increases storage and processing costs. Future improvements could include configurable audit levels or sampling strategies.
  • Service coupling: The authorization service becomes a critical dependency for all other services. This creates a single point of failure that requires careful monitoring and redundancy planning.
  • Schema evolution complexity: As authorization rules become more complex, maintaining backward compatibility in the data model and API becomes increasingly difficult. This debt will compound over time as more granular permissions are added.
  • Testing coverage gaps: The complexity of authorization logic makes comprehensive testing challenging. While unit tests cover basic cases, end-to-end integration testing with various policy combinations is difficult to maintain at scale.

Was this article helpful?

Topics in this article

More from Backend LLD

  • Backend HLD

    Attribute-Based Access Control (ABAC) System Design

    Comprehensive high-level design for a scalable ABAC system with policy evaluation, attribute management, and integration capabilities.

  • Backend HLD

    Role-Based Access Control System Design

    Comprehensive high-level design for a scalable RBAC system with multi-tenant support, audit logging, and real-time permission management.

  • Backend LLD

    Policy Engine Design and Implementation

    Comprehensive low-level design for a flexible, scalable policy engine system supporting rule-based access control and business logic enforcement.

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.