0) Problem Restatement
Design an authorization system for a product like Confluence, Jira or Snowflake. It must answer one question very fast and very often: "Can user U do action A on resource R?"
It supports two models together:
- RBAC (Role-Based Access Control): users (or groups) get roles such as Admin, Editor or Viewer, and each role grants a set of permissions.
- Resource ACLs (Access Control Lists): a specific page or project can grant or deny access to specific users or groups ("share this page with Priya").
Resources are often nested (a space contains pages, and pages contain sub-pages), and permissions usually inherit from the parent.
Asked at: Atlassian, Snowflake, TikTok — 3 candidate reports between Jan 2026 and May 2026.1) Requirements
1.1 Functional
- Manage users, groups (including nested groups), roles and permissions.
- Assign roles at different scopes (whole organization, one project, one page).
- Grant or deny access on individual resources.
- Inheritance: a page follows its space's permissions unless overridden.
check(user, action, resource)→ allow/deny, plus "list resources this user can see".- An audit log of permission changes.
1.2 Non-Functional
- Fast: checks in under ~5 ms, since every request does several.
- Correct: a revoked user must lose access quickly (seconds).
- Scalable: millions of users and resources, 100K+ checks/sec.
1.3 Scale Estimates
- 10M users, 1B resources, 100M ACL entries.
- 200K checks/sec at peak. Most can be served from a cache.
1.4 API Design
POST /v1/check{ user_id, action: "page.edit", resource: "page:123" }→{ allowed: true }POST /v1/roles/assign{ principal: "group:eng", role: "editor", scope: "space:9" }POST /v1/acl{ resource: "page:123", principal: "user:42", permission: "view", effect: "allow|deny" }GET /v1/users/{id}/resources?type=page&action=view(list)
2) High-Level Architecture
2.1 Overview
- Policy Admin API: changes roles, groups and ACLs, and writes to the DB plus an audit log.
- Authorization DB: the relational schema below (source of truth).
- Check Service: evaluates permissions. Runs close to the apps (sidecar or library) with a local cache.
- Change stream: publishes permission changes so caches can be invalidated.
2.2 Architecture Diagram
Architecture Diagram
flowchart LR
APP["Product services"] -->|"check user, action, resource"| CK["Check Service + cache"]
CK --> DB[("Authorization DB")]
ADM["Admin UI / APIs"] --> PA["Policy Admin API"]
PA --> DB
PA --> AUD[("Audit log")]
PA --> K[("Permission change events")]
K -->|"invalidate"| CK3) Data Model
users(user_id), groups(group_id), group_members(group_id, member_type, member_id) -- nested groups
roles(role_id, name) -- Admin, Editor, Viewer
role_permissions(role_id, permission) -- 'page.view', 'page.edit', ...
role_assignments(principal_type, principal_id, role_id, scope_type, scope_id)
resources(resource_id, type, parent_id) -- page → space → org
acl_entries(resource_id, principal_type, principal_id, permission, effect) -- allow / deny
4) How a Check Works
For check(user 42, page.edit, page:123):
- Find the principals: user 42 plus all groups they belong to, following nested groups (cache this per user).
- Walk up the resource tree: page:123 → space:9 → org:1.
- At each level, look for:
- ACL entries for any of the user's principals with
page.edit. - Role assignments at that scope whose role includes
page.edit.
- An explicit deny beats an allow at the same level.
- The nearest level wins (a page-level rule beats a space-level rule), unless the org enforces a rule that can't be overridden.
- The default is deny.
5) Deep Dive A — Making checks fast
- Cache per (user, resource, action) for a short time (e.g., 30–60 seconds), and invalidate on relevant change events.
- Cache the building blocks: a user's group list, and a resource's ancestor chain. These change rarely.
- Precompute effective permissions for hot resources, or materialize "user → roles per scope" to avoid joins.
- For "list everything I can see", don't run a check per resource. Filter inside the search or DB query using the user's principals and scopes, then run a final check on the page of results.
6) Deep Dive B — Scale and consistency (Zanzibar model)
Google's Zanzibar (used for Drive and YouTube) stores everything as relationships: page:123#viewer@group:eng#member. Checks become graph lookups ("is user 42 connected to page:123 via viewer?"). Open-source versions include SpiceDB and OpenFGA.
- It scales by sharding relationships and caching sub-results heavily.
- The "new enemy" problem: Alice removes Bob from a doc, then adds secret content. If a stale cache still lets Bob in, he sees the secret. Zanzibar fixes this with a consistency token (a "zookie") returned when content changes. Checks for that content must use data at least as fresh as the token.
- A simpler approach for most systems: invalidate caches on every permission change, keep cache TTLs short, and for sensitive actions bypass the cache.
7) Trade-offs & Alternatives
| Decision | Choice | Why | Alternative |
|---|---|---|---|
| Model | RBAC + resource ACLs with inheritance | Covers org roles and sharing | RBAC only: can't share single items |
| Storage | Relational schema | Clear, easy to audit | Relationship graph (Zanzibar): scales further, more complex |
| Speed | Check service with cache + invalidation | Few-ms checks | Query DB on every check: slow |
| Conflicts | Deny wins, nearest wins, default deny | Safe and predictable | Allow wins: risky |
8) Common Follow-up Questions
- "Attribute-based rules?" (e.g., "only during work hours" or "only from the company network"). Add an ABAC layer that evaluates conditions (a policy engine like OPA) after RBAC passes.
- "Multi-tenant?" Every row carries a
tenant_id, and a check never crosses tenants. - "Audit?" Log every permission change (who, what, when), and optionally sampled or all denied checks for security review.
9) Wrap-Up
Store users, nested groups, roles, role assignments per scope and resource ACLs in a relational schema. Resolve a check by expanding the user's groups, walking up the resource hierarchy, and applying clear rules (deny wins, nearest wins, default deny). Serve checks from a nearby service with caches invalidated by change events, and move to a Zanzibar-style relationship store with consistency tokens when scale and strictness demand it.