Compile Ready
All low level design problems
Low Level Design/Advanced/Expert Systems

Design a Role-Based Access Control (RBAC) System

Users, roles, permissions, and resource-level checks with hierarchical roles and policy evaluation.

Advanced 55m interview 14m read Medium frequency Popularity 79
Composite Strategy Chain of Responsibility Amazon Microsoft Atlassian Oracle

Problem Statement

Design a Role-Based Access Control system that decides whether a user may perform an action on a resource. The model must support users, roles, permissions, resource/action checks, and role hierarchy where one role inherits permissions from other roles.

The core interview challenge is to keep authorization checks fast while still making role composition easy to reason about. A good design resolves inherited permissions once, caches them per user, and answers hot-path checks with hash-set membership rather than walking the hierarchy on every request.

Business context

RBAC sits in the critical path of admin consoles, enterprise SaaS products, developer platforms, and internal tools. A slow or inconsistent check either blocks legitimate work or exposes protected data.

Interviewers use this problem to test whether you can separate identity from authorization, model hierarchical roles without cycles, apply Composite cleanly, and explain the tradeoff between flexible policy evaluation and O(1)-ish runtime checks.

Functional Requirements

  • Create and store users with stable ids.

  • Create roles with direct permissions such as read invoice or delete project.

  • Allow a role to inherit permissions from one or more other roles through a composite hierarchy.

  • Grant roles to users and derive the user's effective permission set from all assigned roles.

  • Represent protected resources by type, id, and owner so checks can be expressed as action on resource.

  • Authorize a request by user id, resource, and action.

  • Support wildcard permissions such as all actions on invoices or a platform-wide super-user permission.

  • Invalidate cached user permissions when roles, role inheritance, or grants change.

Non-Functional Requirements

Low-latency authorization

The read path should be close to O(1): compute a Permission key from resource type and action, then check a cached hash set.

Hierarchy safety

Composite roles must reject cycles. A role inheriting itself, directly or indirectly, would make permission resolution ambiguous and unsafe.

Auditability

Every authorization decision should be observable at the boundary without putting logging concerns into the core engine.

Extensibility

New permission matching rules, owner-based rules, or deny policies should be additive through a strategy rather than a rewrite of every role and user class.

Deterministic behavior

Given the same users, roles, permissions, and resources, the engine should return the same answer regardless of traversal order.

Requirement Clarification

QAre permissions attached directly to users?

No for the base design. Users receive roles, and roles carry permissions. Direct user permissions can be added later, but keeping the base model role-centric makes auditing simpler.

QDoes a child role inherit from a parent role or the other way around?

Use the phrasing Admin inherits Editor. Admin's effective permission set includes Admin's direct permissions plus Editor's effective permissions.

QDo we need allow and deny decisions?

Start with allow-only RBAC. Deny policies are an extension because they change conflict resolution and require explicit precedence rules.

QWhat counts as O(1)-ish permission checking?

The hot check should avoid hierarchy traversal. It may perform a constant number of hash-set lookups for exact and wildcard candidates.

QIs this an authentication system too?

No. Authentication proves who the caller is. This design assumes a trusted user id and answers what that user may do.

UML Class Diagram

Rendering diagram…
Role is the Composite: a role contains permissions and other roles. AccessControlService owns the registry and cache, while PermissionStrategy decides how resource/action pairs map to membership checks.

Sequence Diagram

Rendering diagram…
The expensive part is flattening role inheritance, and it happens only on cache miss or after invalidation. The common decision path is a fixed set of hash lookups.

Entity Identification

User

Represents the subject being authorized. It stores identity fields and assigned roles, but does not decide access by itself.

idnameroles

Role

Composite permission container. A role owns direct permissions and can inherit other roles, so effective permissions are the union across a role graph.

idnamepermissionsinheritedRoles

Permission

Immutable value object for resourceType + action. Equality and hashing make permission checks hash-set based.

resourceTypeaction

Resource

Protected object being accessed. The engine uses its type for permission matching and keeps id plus owner for future resource-specific policies.

typeidownerUserId

PermissionStrategy

Policy seam for evaluating a cached permission set against a resource/action request. The default strategy checks exact and wildcard permissions.

isAllowed(permissions, resource, action)

AccessControlService

Singleton authorization engine. It creates users and roles, grants roles, invalidates caches, and answers authorization decisions.

usersrolesuserPermissionCachepermissionStrategy

AuthorizationProxy

Boundary object that validates and audits authorization calls before delegating to the service. It keeps observability separate from core rules.

delegatecan(userId, resource, action)

Design Patterns Used

Composite

Role contains direct permissions and inherited roles. Effective permissions are resolved by recursively visiting child roles, which models role hierarchy without special-case code for leaf and composite roles.

Strategy

PermissionStrategy isolates the matching algorithm. The default strategy supports exact and wildcard permissions, while future owner-aware or tenant-aware strategies can plug in without changing users or roles.

Proxy

AuthorizationProxy wraps the authorization engine to perform request validation and audit logging. It controls access to the service without mixing logging into the permission model.

Singleton

AccessControlService.getInstance gives the demo one canonical in-memory registry and cache. In production, the same boundary could be dependency-injected, but the singleton keeps the sample focused.

Step-by-Step Design

  1. 1Represent permissions as immutable hash keys

    A permission should be a value object, not a mutable record. Normalize resource type and action so INVOICE:READ and invoice:read do not become different grants.

    public final class Permission {
        private final String resourceType;
        private final String action;
    
        public static Permission of(String resourceType, String action) {
            return new Permission(resourceType, action);
        }
    }
  2. 2Model role hierarchy with Composite

    A role is both a permission holder and a container of other roles. Resolving a role means unioning its direct permissions with inherited roles, while cycle checks protect the graph.

    public Role inherit(Role role) {
        if (role == this || role.dependsOn(this, new HashSet<>())) {
            throw new IllegalArgumentException("Role inheritance cycle");
        }
        inheritedRoles.add(role);
        return this;
    }
  3. 3Cache flattened permissions per user

    Granting a role invalidates only that user's cache. Editing a role invalidates all user caches because many users may depend on that role through inheritance.

  4. 4Use a strategy for resource/action matching

    The engine asks PermissionStrategy whether a set allows a request. The default strategy checks exact permission, any action on a resource type, global action, and super-user wildcard.

    return permissions.contains(Permission.of(resource.type(), action))
            || permissions.contains(Permission.anyAction(resource.type()))
            || permissions.contains(Permission.global(action))
            || permissions.contains(Permission.superUser());
  5. 5Put audit and validation behind a proxy

    AuthorizationProxy measures latency and logs the decision after delegation. This keeps the domain model deterministic and easy to test.

  6. 6Keep the public service API small

    Expose creation, role mutation, grants, and isAllowed. Avoid exposing mutable maps or sets; callers should not be able to corrupt the cache or role graph.

Complete Java Implementation

Loading…

Explanation of Every Class

Permission

Immutable value object for authorization keys. It normalizes resource type and action, supports exact grants and wildcard grants, and implements equality/hashCode so hash-set checks are constant time on average.

Resource

Small descriptor for the protected object. The base strategy uses type for permission lookup, while id and ownerUserId leave room for instance-level and owner-aware policies.

Role

Composite node in the role hierarchy. It stores direct permissions and inherited roles, rejects inheritance cycles, and resolves effective permissions by unioning the reachable role graph.

User

Subject being authorized. It owns a set of assigned roles and exposes only an immutable view so callers cannot bypass the service cache invalidation rules.

PermissionStrategy

Strategy interface for checking whether a permission set allows a resource/action request. It keeps matching rules outside User and Role.

ExactAndWildcardPermissionStrategy

Default strategy implementation. It performs four hash-set probes: exact resource/action, any action on the resource type, global action, and super-user.

AccessControlService

Singleton registry, cache owner, and authorization engine. It owns user and role maps, mutates role definitions through cache-safe methods, and evaluates checks through the strategy.

AuthorizationProxy

Audited boundary around the service. It delegates the decision, measures latency, and logs the result without adding cross-cutting concerns to the domain model.

Main

Demonstrates role inheritance: editor inherits reader, admin inherits editor and has wildcard invoice access, then Alice and Bob receive different effective permissions.

Dry Run

Sample input

Roles: Reader has invoice:read, Editor inherits Reader and adds invoice:write, Admin inherits Editor and adds invoice:*. Alice has Admin, Bob has Reader. Resource is invoice inv-100.

StepRequestCache statePermission candidatesDecisionReason
1Alice delete invoiceMiss then cache {invoice:read, invoice:write, invoice:*}invoice:delete, invoice:*, *:delete, *:*Allowinvoice:* is present through Admin
2Bob write invoiceMiss then cache {invoice:read}invoice:write, invoice:*, *:write, *:*DenyNo candidate permission is present
3Alice read invoiceHitinvoice:read, invoice:*, *:read, *:*AllowExact read and wildcard invoice permission are both present
4Add export to ReaderAll user caches clearedNo check yetN/ARole mutation can affect many users, so cached sets are invalidated
5Bob export invoiceMiss then cache {invoice:read, invoice:export}invoice:export, invoice:*, *:export, *:*AllowNew Reader permission is included after recomputation

The first request pays the hierarchy-resolution cost. Later checks for the same user are a fixed number of hash lookups until grants or role definitions change.

Complexity Analysis

OperationTimeSpaceNote
isAllowed with warm user cacheO(1)O(1)The default strategy performs four hash-set contains checks.
isAllowed with cold user cacheO(R + E + P)O(P)R assigned/reachable roles, E inheritance edges visited, P unique permissions flattened into the user cache.
grantRoleToUserO(1)O(1)Adds a role reference and clears only that user's cached effective permissions.
addPermissionToRole or makeRoleInheritO(1) to O(E)O(1)Adding a permission is constant; inheritance checks may traverse role edges to prevent cycles, then all user caches are cleared.
effectivePermissions for a roleO(E + P)O(R + P)DFS over inherited roles plus a set of accumulated permissions.

The design intentionally moves work from the read path to mutation/cache-miss paths. That is usually the right RBAC tradeoff because authorization checks happen far more often than role definition changes.

Extensibility

Instance-level permissions

Extend Permission with an optional resource id or scope field, then update PermissionStrategy to check exact instance keys before type-level keys.

Owner-aware access

Add a strategy that allows actions such as read own invoice when resource.ownerUserId matches the current user id. The service API may pass the user into the strategy.

Explicit deny policies

Introduce separate allow and deny sets and define precedence clearly, usually deny wins. This belongs in a new strategy because it changes decision semantics.

Persistent repositories

Replace in-memory maps with UserRepository, RoleRepository, and PermissionRepository while keeping the AccessControlService boundary intact.

Multi-tenant authorization

Add tenant id to User, Role, Resource, and cache keys so one tenant's Admin role cannot authorize another tenant's resources.

Alternative Designs

Evaluate role graph on every request

Skip user permission caching and traverse assigned roles plus inherited roles for every authorization check.

Tradeoffs

Simpler invalidation, but hot checks become O(R + E + P) and latency grows with hierarchy depth.

Persist a flattened user-permission table

Materialize user id to permission rows in a database and update them asynchronously or transactionally whenever grants and roles change.

Tradeoffs

Excellent read latency and cross-process consistency, but role mutations become more expensive and require careful rebuild jobs.

Policy engine instead of pure RBAC

Use a rule language or attribute-based access control engine where permissions depend on user, resource, environment, and relationship attributes.

Tradeoffs

More expressive than RBAC, but harder to explain, cache, audit, and test in a short LLD interview.

Common Mistakes

  • ×

    Walking the entire role hierarchy on every authorization request and then claiming the check is O(1).

  • ×

    Allowing role inheritance cycles, which can cause infinite recursion or inconsistent permission sets.

  • ×

    Mixing authentication and authorization so login state leaks into the RBAC model.

  • ×

    Putting audit logging inside Role or Permission, making the domain model hard to test.

  • ×

    Returning mutable internal sets from users or roles, letting callers bypass cache invalidation.

  • ×

    Using string comparisons everywhere instead of a normalized Permission value object.

  • ×

    Forgetting that role definition changes must invalidate users who inherit that role indirectly.

Follow-up Interview Questions

QHow do you make authorization checks O(1)?

Flatten inherited role permissions into a cached set per user. A check then creates a small number of candidate Permission keys and uses hash-set membership.

QHow do you prevent cycles in role hierarchy?

When adding A inherits B, search from B to see whether A is already reachable. If yes, reject the edge because it would create a cycle.

QWhat changes when explicit deny is introduced?

You need conflict resolution. Most systems make deny override allow, so the strategy must check deny candidates first and document precedence.

QHow would this work across multiple application instances?

Persist roles and grants, publish invalidation events on changes, and use versioned cache entries so every instance refreshes stale user permission sets.

QWhy use a proxy instead of logging inside AccessControlService?

The proxy keeps cross-cutting concerns at the boundary. The service remains focused on authorization state and decisions, making it easier to unit test.

Production Considerations

Cache invalidation

Use role version numbers or authorization policy versions in cache keys. On role changes, publish events so every service instance evicts affected users.

Audit logs

Record user id, resource type, resource id, action, decision, policy version, and latency. Avoid logging sensitive resource payloads.

Least privilege

Prefer narrowly scoped roles, periodic access reviews, and break-glass roles with extra approval. RBAC designs fail when every user becomes Admin.

Tenant isolation

Include tenant id in every role, resource, and cache key. Treat cross-tenant role grants as invalid by default.

Operational safety

Make permission changes observable and reversible. Keep a history of who changed a role and which users gained or lost permissions.

What Interviewers Look For

  • Did the candidate separate User, Role, Permission, Resource, and authorization engine responsibilities?

  • Did they model role hierarchy with a clear Composite and cycle prevention?

  • Did they explain why cached flattened permissions make hot checks O(1)-ish?

  • Did they provide an invalidation story for grants and role mutations?

  • Did they keep policy matching behind a Strategy instead of spreading if statements across the model?

  • Did they recognize audit logging as a boundary concern suitable for a Proxy?

Quiz

0/5 answered

  1. 1.Why is **Role** a good use of the Composite pattern in this design?

  2. 2.What makes the warm authorization path O(1)-ish?

  3. 3.What should happen when a role gains a new permission?

  4. 4.Why is **PermissionStrategy** useful?

  5. 5.Which edge should be rejected to prevent a cycle?

Practice Variants

Add explicit deny rules

Advanced

Introduce deny permissions and define precedence. Update the strategy so deny candidates are checked before allow candidates.

Add resource ownership checks

Intermediate

Support permissions like read own invoice by passing the user into the strategy and comparing it with resource.ownerUserId.

Add tenant isolation

Advanced

Require every role and resource to belong to a tenant, and ensure the cache key includes tenant id.

Flashcards

Cheat Sheet

Entities: User, Role, Permission, Resource, PermissionStrategy, AccessControlService, AuthorizationProxy.

Role hierarchy: Admin inherits Editor; Editor inherits Reader. Effective permissions are the union of all reachable roles.

Patterns: Composite for Role inheritance, Strategy for permission matching, Proxy for audited access, Singleton for the demo service registry.

Hot path: user id + resource + action → cached user permission set → exact and wildcard Permission candidates → hash-set contains.

Invalidation: grant changes clear one user cache; role permission and hierarchy changes clear or version affected user caches.

Complexity: warm check O(1), cold check O(R + E + P), role mutation includes cycle detection and cache invalidation.

Extensions: explicit deny, resource ownership, instance-level permissions, tenant isolation, persistent repositories.

References

  • PaperNIST RBAC ModelNIST
  • BookEnterprise Integration Patterns — Proxy and Gateway ConceptsGregor Hohpe and Bobby Woolf
  • BookDesign Patterns: Elements of Reusable Object-Oriented SoftwareGamma, Helm, Johnson, and Vlissides
  • DocsOWASP Authorization Cheat SheetOWASP