Design a Role-Based Access Control (RBAC) System
Users, roles, permissions, and resource-level checks with hierarchical roles and policy evaluation.
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
Sequence Diagram
Entity Identification
User
Represents the subject being authorized. It stores identity fields and assigned roles, but does not decide access by itself.
Role
Composite permission container. A role owns direct permissions and can inherit other roles, so effective permissions are the union across a role graph.
Permission
Immutable value object for resourceType + action. Equality and hashing make permission checks hash-set based.
Resource
Protected object being accessed. The engine uses its type for permission matching and keeps id plus owner for future resource-specific policies.
PermissionStrategy
Policy seam for evaluating a cached permission set against a resource/action request. The default strategy checks exact and wildcard permissions.
AccessControlService
Singleton authorization engine. It creates users and roles, grants roles, invalidates caches, and answers authorization decisions.
AuthorizationProxy
Boundary object that validates and audits authorization calls before delegating to the service. It keeps observability separate from core rules.
Design Patterns Used
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.
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.
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.
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
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); } }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; }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.
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());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.
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
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.
| Step | Request | Cache state | Permission candidates | Decision | Reason |
|---|---|---|---|---|---|
| 1 | Alice delete invoice | Miss then cache {invoice:read, invoice:write, invoice:*} | invoice:delete, invoice:*, *:delete, *:* | Allow | invoice:* is present through Admin |
| 2 | Bob write invoice | Miss then cache {invoice:read} | invoice:write, invoice:*, *:write, *:* | Deny | No candidate permission is present |
| 3 | Alice read invoice | Hit | invoice:read, invoice:*, *:read, *:* | Allow | Exact read and wildcard invoice permission are both present |
| 4 | Add export to Reader | All user caches cleared | No check yet | N/A | Role mutation can affect many users, so cached sets are invalidated |
| 5 | Bob export invoice | Miss then cache {invoice:read, invoice:export} | invoice:export, invoice:*, *:export, *:* | Allow | New 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
| Operation | Time | Space | Note |
|---|---|---|---|
| isAllowed with warm user cache | O(1) | O(1) | The default strategy performs four hash-set contains checks. |
| isAllowed with cold user cache | O(R + E + P) | O(P) | R assigned/reachable roles, E inheritance edges visited, P unique permissions flattened into the user cache. |
| grantRoleToUser | O(1) | O(1) | Adds a role reference and clears only that user's cached effective permissions. |
| addPermissionToRole or makeRoleInherit | O(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 role | O(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.Why is **Role** a good use of the Composite pattern in this design?
2.What makes the warm authorization path O(1)-ish?
3.What should happen when a role gains a new permission?
4.Why is **PermissionStrategy** useful?
5.Which edge should be rejected to prevent a cycle?
Practice Variants
Add explicit deny rules
AdvancedIntroduce deny permissions and define precedence. Update the strategy so deny candidates are checked before allow candidates.
Add resource ownership checks
IntermediateSupport permissions like read own invoice by passing the user into the strategy and comparing it with resource.ownerUserId.
Add tenant isolation
AdvancedRequire 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 Model — NIST
- BookEnterprise Integration Patterns — Proxy and Gateway Concepts — Gregor Hohpe and Bobby Woolf
- BookDesign Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, and Vlissides
- DocsOWASP Authorization Cheat Sheet — OWASP