Zanzibar-Style Authorization Service System Design Template
Relationship-based authorization service: relation tuples, namespace config, Check/Expand/List/Watch APIs, consistency tokens, caching, sharding, failure modes
使い方: Use it when several products need one place to decide who can access what, with sharing, groups and folder inheritance. Fill the namespace configuration and the consistency contract (when clients pass a token) first. The main pitfall is serving checks from stale snapshots, the new-enemy problem, because clients drop the token returned by writes.
プレビュー
System Design: [Authorization service name]
Reference design for a relationship-based authorization service in the style of Google Zanzibar: access rules are stored as relation tuples (object, relation, subject), a namespace configuration defines how relations combine and inherit, and applications call Check, Expand, List and Watch APIs instead of embedding permission logic. It covers consistency tokens ("zookies") and the new-enemy problem, check evaluation and caching, deeply nested groups, reverse lookups, latency SLOs and sharding of the tuple store. Concepts follow Pang et al., "Zanzibar: Google's Consistent, Global Authorization System" (USENIX ATC 2019) and the public documentation of the open-source OpenFGA and SpiceDB projects. It follows the 12-section structure of the System Design Document template. Replace every
[bracketed]value and every row marked Example.
Document control
| Field | Value |
|---|---|
| Document ID | [SDD-AUTHZ-001] |
| System / feature | [Central authorization service] |
| Version | [0.1] |
| Status | [Draft / In review / Approved / Implemented / Superseded] |
| Owner | [@owner] |
| Author(s) | [@author] |
| Reviewers | [@reviewer-1], [@reviewer-2] |
| Last updated | YYYY-MM-DD |
| Classification | [Internal] |
| Schema owner | [@team] |
| Related | [Identity provider design], [Threat model], [ADR: consistency contract] |
Approvals
| Role | Name | Decision | Date |
|---|---|---|---|
| Tech lead | [@name] | [Approved / Changes requested] | YYYY-MM-DD |
| Staff / principal engineer | [@name] | [Decision] | YYYY-MM-DD |
| Security architect | [@name] | [Decision] | YYYY-MM-DD |
| SRE / operations | [@name] | [Decision] | YYYY-MM-DD |
| Representative client team | [@name] | [Decision] | YYYY-MM-DD |
Revision history
| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft for design review |
| 0.2 | YYYY-MM-DD | [@name] | Example: Added group membership index after nested-group latency tests |
| 0.3 | YYYY-MM-DD | [@name] | Example: Consistency contract changed from bounded staleness to tokens |
| 1.0 | YYYY-MM-DD | [@name] | Example: Approved |
Contents
1. Overview
1.1 Problem statement
[Example: Each product (documents, projects, billing) implements its own permission checks against its own tables. Sharing a folder with a group works in the documents product but not in search, which shows titles the user cannot open. A permissions bug last quarter exposed private projects to former team members because the project service cached roles for 30 minutes after removal. Adding a new sharing feature takes each team weeks.]
1.2 Proposed solution
[Example: A central authorization service stores every access fact as a
relation tuple, such as doc:readme#viewer@group:eng#member. A versioned
namespace configuration defines how relations combine: an editor is also a
viewer, and a viewer of a folder is a viewer of every document in it.
Products call Check on every request, ListObjects to filter search
results, and Write when users share or unshare. Every write and check
returns a consistency token; when content changes, the product stores a
fresh token with the new content version, and later checks pass it so a
check never uses data older than the last permission change that matters. Tuples live in a sharded, replicated
database; check results are cached per snapshot and distributed across
servers by consistent hashing.]
1.3 Decisions requested from reviewers
| ID | Question | Recommended option | Section |
|---|---|---|---|
| Q-01 | Example: Consistency contract for clients? | Tokens with "at least as fresh" semantics; full consistency only on request | 7.1 |
| Q-02 | Example: Tuple store technology? | Distributed SQL with external-consistency timestamps | 5.4, 10 |
| Q-03 | Example: How to handle groups nested 10 levels deep? | Precomputed group membership index for selected namespaces | 7.3 |
| Q-04 | Example: How do search and list pages filter results? | ListObjects for small result sets; pre-filter index for search | 7.4 |
| Q-05 | Example: Fail open or closed when the service is unreachable? | Fail closed, with a short client-side cache of positive results | 9.1 |
| [Q-nn] | [Question] | [Option] | [Section] |
1.4 45-minute interview path
| Minutes | Cover | Output on the whiteboard |
|---|---|---|
| 0 to 5 | Clarify: who calls, what objects, roles or relationships, scale, consistency expectation | Core and below-the-line lists (2.1, 2.3) |
| 5 to 10 | Estimate checks per second, tuples, fan-out per check | Three numbers and their consequences (3.6) |
| 10 to 15 | API: Check, Write, Read, Expand, ListObjects, Watch | Signatures with the consistency token (4.1) |
| 15 to 20 | Data model: tuple, namespace configuration, access patterns | Tuple table and one rewrite rule (5.2, 5.3) |
| 20 to 28 | High-level design: check servers, tuple store, cache, changelog | One diagram (6) |
| 28 to 40 | One deep dive: the new-enemy problem and tokens (7.1) is the strongest choice; caching (7.2) if the interviewer pushes on latency | Options, choice, reason |
| 40 to 45 | Failure modes, fail-closed decision, what changes at 10x | Two rows of the failure table (9.1) |
2. Requirements
2.1 Core functional requirements
| ID | Requirement | Priority |
|---|---|---|
| FR-01 | Example: Answer "does subject S have relation R on object O?" (Check) | Must |
| FR-02 | Example: Store and delete relation tuples, with optional preconditions (Write) | Must |
| FR-03 | Example: Support usersets: a subject can be "members of group G" | Must |
| FR-04 | Example: Support relation inheritance within an object (owner implies editor implies viewer) | Must |
| FR-05 | Example: Support inheritance across objects (viewer of a folder views its documents) | Must |
| FR-06 | Example: Return a consistency token on every write; accept one on every read | Must |
| FR-07 | Example: List objects of a type a subject can access (ListObjects) | Must |
| FR-08 | Example: Show why access is granted (Expand, as an effective userset tree) | Should |
| FR-09 | Example: Stream tuple changes to subscribers in commit order (Watch) | Should |
| FR-10 | Example: Version the namespace configuration and reject unsafe changes | Must |
| [FR-nn] | [Requirement] | [Must / Should / Could] |
2.2 Non-functional requirements
| ID | Quality | Target | Measured as |
|---|---|---|---|
| NFR-01 | Check latency | Example: p95 at or below 10 ms, p99 at or below 30 ms, in region | Server-side timing per API |
| NFR-02 | Availability | Example: 99.99% monthly for Check | Successful responses / valid requests |
| NFR-03 | Correctness | Example: No check evaluated at a snapshot older than the token supplied | Consistency test suite with injected lag |
| NFR-04 | Throughput | Example: 500 000 checks/s at peak, 5 000 tuple writes/s | Load test |
| NFR-05 | Write latency | Example: p99 at or below 100 ms | Server-side timing |
| NFR-06 | Isolation | Example: One tenant's hot object cannot raise p99 for others | Hot-spot test |
| [NFR-nn] | [Quality] | [Target] | [Measurement] |
2.3 Below the line (out of scope)
- [Example: authentication and user identity (the identity provider issues
sub; see Identity Provider and SSO System Design Template (system-design/design-identity-sso-provider))] - [Example: attribute conditions such as time of day or IP range (phase 2, as caveats evaluated on top of tuples)]
- [Example: policy-as-code for infrastructure (stays in the existing policy engine)]
- [Example: end-user audit log UI (products read the Watch stream)]
- [Item deferred, with the release that will take it]
3. Capacity estimation
3.1 Assumptions
| Input | Symbol | Value | Source |
|---|---|---|---|
| Peak checks per second, all regions | Example: 500 000 | Gateway metrics of all products x growth | |
| Peak tuple writes per second | Example: 5 000 | Sharing and membership events | |
| Stored tuples | Example: 20 billion | Objects x average ACL entries | |
| Bytes per tuple including index overhead | Example: 100 B | Schema in 5.2 | |
| Replication factor | Example: 3 | Store configuration | |
| Average storage reads per uncached check | Example: 4 | Fan-out measured on the schema in 5.3 | |
| Cache hit ratio for sub-problems | Example: 0.8 | Prototype measurement | |
| Reads per second one shard serves at target latency | Example: 20 000 | Benchmark |
3.2 Storage
Consequence: 2 TB of logical data does not fit on one node with headroom, so the tuple store is sharded from day one, by object (section 5.4).
3.3 Storage reads
Consequence: the cache hit ratio is the dominant lever. If drops from 0.8 to 0.6, reads double to 800 000/s and the shard count doubles, so the cache gets its own deep dive (7.2) and its own SLO. Each shard then holds about 6 TB / 32, or roughly 190 GB, which is comfortable.
3.4 Changelog
Consequence: seven days of Watch history is about 600 GB, small enough to keep in the same store as a time-ordered table, so Watch needs no separate log system at this scale.
3.5 Latency
A check that walks six levels of nesting with one storage read per level costs six sequential round trips. At 1 to 2 ms per read that is 6 to 12 ms, already at the p95 budget. Consequence: evaluate branches of a union in parallel, cache intermediate results, and index deeply nested groups (7.3).
3.6 Summary
| Quantity | Estimate | Design consequence |
|---|---|---|
| Checks | 500 000/s | Stateless check servers, scaled horizontally |
| Logical tuple data | 2 TB (6 TB replicated) | Shard by object from day one |
| Storage reads | 400 000/s at 80% hit | 32 shards; cache hit ratio monitored as an SLO |
| Changelog | ~86 GB/day | Watch served from a changelog table, 7-day retention |
| Nested check | 6 to 12 ms if sequential | Parallel evaluation, caching, group index |
4. API design
4.1 Operations
| Operation | Input | Output | Notes |
|---|---|---|---|
Check | object, relation, subject, consistency | allowed (bool), checked_at token | Hot path; must be cheap |
BatchCheck | up to [100] checks, consistency | list of results | For list pages |
Write | tuples to add, tuples to delete, preconditions | token | Atomic per request |
Read | tuple filter, consistency, page token | tuples, next page token | Admin and debugging |
Expand | object, relation, consistency | userset tree | Explains effective access |
ListObjects | object type, relation, subject, consistency | object IDs (bounded) | Reverse lookup (7.4) |
ListSubjects | object, relation, subject type, consistency | subject IDs (bounded) | Who can see this |
Watch | namespaces, start token | stream of tuple changes with tokens | Commit order per shard |
WriteSchema | namespace configuration | schema version | Validated before activation (7.5) |
Consistency modes accepted by every read operation:
| Mode | Meaning | Typical use |
|---|---|---|
minimize_latency | Any recent snapshot the server chooses | Low-risk reads such as UI hints |
at_least_as_fresh(token) | Snapshot no older than the token | Default for content checks |
at_exact_snapshot(token) | Exactly the token's snapshot | Paging through Read or List results |
fully_consistent | Latest committed data | Right after a security-sensitive change |
4.2 Request and response examples
A check that protects content changed after a permission write passes the token saved with that content. A write that removes one viewer and adds a group is atomic, guarded by a precondition on the owner tuple.
POST /v1/check
Content-Type: application/json
{"object": "doc:readme", "relation": "view", "subject": "user:alice",
"consistency": {"at_least_as_fresh": "zk_CAESBgjM8tK9Bg"}}
HTTP/1.1 200 OK
{"allowed": true, "checked_at": "zk_CAESBgjQ8tK9Bg"}
POST /v1/write
Content-Type: application/json
{"writes": [{"object": "doc:readme", "relation": "viewer", "subject": "group:eng#member"}],
"deletes": [{"object": "doc:readme", "relation": "viewer", "subject": "user:mallory"}],
"preconditions": [{"must_exist": {"object": "doc:readme", "relation": "owner", "subject": "user:alice"}}]}
HTTP/1.1 200 OK
{"written_at": "zk_CAESBgjR8tK9Bg"}
4.3 Errors and limits
| Condition | Status | Body type |
|---|---|---|
| Unknown namespace or relation | 400 | problems/schema-mismatch |
| Precondition failed on Write | 409 | problems/precondition-failed |
| Token older than retained history | 410 | problems/snapshot-expired (retry with at_least_as_fresh) |
| Check exceeded depth limit [25] | 422 | problems/max-depth |
| Tenant quota exceeded | 429 | RFC 9457 problem details with Retry-After |
| Deadline exceeded | 504 | Client treats as deny (9.1) |
5. Data model
5.1 Entities
Tuples are the bulk of the data; the configuration, changelog and group index are derived from or describe them.
5.2 Tuple format
A tuple is object#relation@subject, where the subject is either a direct
subject (user:alice) or a userset (group:eng#member, meaning "every
subject with relation member on group eng"). Example tuples:
| Tuple | Meaning |
|---|---|
doc:readme#owner@user:alice | Alice owns the document |
doc:readme#viewer@group:eng#member | Members of group eng can view |
doc:readme#parent@folder:handbook | The document sits in folder handbook |
group:eng#member@group:backend#member | Group backend is nested in eng |
5.3 Namespace configuration
Rewrite rules compute a relation from other relations. The Example below uses a neutral notation; translate it into the schema language of the engine you choose.
type user
type group
relation member: user | group#member
type folder
relation owner: user
relation viewer: user | group#member
permission view = viewer + owner
type doc
relation parent: folder
relation owner: user
relation editor: user | group#member
relation viewer: user | group#member
permission edit = editor + owner
permission view = viewer + edit + parent->view
+ is union; parent->view follows the parent tuple and checks view on
the folder (a tuple-to-userset rewrite). Intersection and exclusion
(viewer & not blocked) are possible but expensive; allow them only where a
reviewer has measured the fan-out.
5.4 Access patterns
| ID | Query | Served by | Partition key rationale |
|---|---|---|---|
| AP-01 (Example) | Direct tuples for (object, relation) during Check | Primary key prefix (namespace, object_id, relation) | Shard by (namespace, object_id) keeps an object's ACL on one shard |
| AP-02 (Example) | Is subject a direct member of userset? | Same key plus subject columns | Point read on the object's shard |
| AP-03 (Example) | Follow parent from a document | AP-01 on relation parent | Same shard as the document |
| AP-04 (Example) | Groups containing a member, transitively | GROUP_INDEX by member_id (7.3) | Separate index sharded by member |
| AP-05 (Example) | Objects a subject can view (ListObjects) | Reverse index by (subject, namespace) plus check filter (7.4) | Secondary index sharded by subject |
| AP-06 (Example) | Changes since token for Watch | CHANGELOG by (shard, commit_ts) | Time order per shard; merged by the Watch service |
| AP-07 (Example) | Current schema | NAMESPACE_CONFIG, cached in every server | Tiny, global, versioned |
Consequence of AP-01: a "hot" object such as a company-wide group lives on one shard. Section 7.2 handles it with caching rather than by splitting the object.
6. High-level design
| Component | Responsibility | Scales by |
|---|---|---|
| Check servers | Parse request, pick snapshot, evaluate rewrite rules, fan out | Stateless replicas per region |
| Result cache | Cache sub-problem results keyed by (object, relation, subject, snapshot) | Memory per server; peers own key ranges |
| Tuple store | Durable tuples with commit timestamps, multi-region replication | Shards by object |
| Changelog | Ordered record of every tuple change | Shards, retained 7 days |
| Watch service | Merge changelog shards and stream to subscribers | Subscribers |
| Group index | Flattened transitive group membership | Members |
| Schema admin | Validate, version and roll out namespace configuration | Single service |
7. Detailed design
7.1 Deep dive: consistency and the new-enemy problem
Problem. A permission change and a content change arrive in order, but a check reads a stale replica and evaluates the new content against the old permissions. Two classic forms:
- Alice removes Bob from a folder, then adds a confidential document to it. A check that sees the document but not the removal lets Bob read it.
- Alice removes Bob from a document's viewers, then asks Charlie to add sensitive text. A check that ignores the order of the two changes lets Bob see the new text.
| Option | How | Cost | Verdict |
|---|---|---|---|
| A. Always fully consistent | Every check reads the latest committed data | Cross-region reads on every check; caches useless | Rejected: misses NFR-01 |
| B. Bounded staleness | Checks read a snapshot at most N seconds old | Window of N seconds where the new-enemy problem occurs | Rejected for content; acceptable for UI hints |
| C. Consistency tokens | Write returns a token encoding its commit timestamp; the product stores it with the content version; checks pass it as "at least as fresh" | Clients must store and forward tokens | Chosen |
Choice and reason. Option C makes freshness proportional to need. Most checks run at a snapshot a few seconds old, which caches well, yet any check that protects content changed after a permission change is guaranteed to see that change. The tuple store must assign commit timestamps that respect real-time order across shards (external consistency); a store without this property breaks the guarantee for multi-shard writes.
Client contract. On every content change, run a content-change check
(fully_consistent, or at least as fresh as any permission write the
product just made) and store the returned checked_at token next to the new
content version (for example in the document row). Storing only the token of
an earlier permission write is not enough when another user changes the
content later. Pass the stored token on every check of that content. When no token exists, use minimize_latency for
listings and fully_consistent only for security administration pages.
7.2 Deep dive: check evaluation and caching
| Option | How | Cost | Verdict |
|---|---|---|---|
| A. No cache | Every check reads storage | 2 million reads/s at peak (3.3 with ) | Rejected |
| B. Per-server cache | Each server caches its own results | Low hit ratio as fleet grows; hot keys duplicated everywhere | Partial |
| C. Distributed cache by consistent hashing | Each sub-problem key has an owner server; servers forward sub-checks to owners | One extra in-cluster hop for some sub-checks | Chosen |
Choice and reason. Snapshots are quantized (for example rounded down to the nearest second) so that many checks share one snapshot and therefore share cache keys. The owner of a key deduplicates concurrent identical requests: if a thousand requests ask about the same company-wide group at the same snapshot, one storage read serves all of them. Cache entries never need invalidation because they are keyed by snapshot; they expire by age.
Evaluation rules:
- Evaluate union branches in parallel and return on the first
true. - Cap recursion depth (4.3) and detect cycles with a visited set.
- Hedge slow storage reads: after the p95 latency of that shard, send the same read to another replica and take the first answer.
- Never cache a result computed at a snapshot newer than the one requested under a key for the older snapshot.
7.3 Deep dive: deeply nested groups
Organisations nest groups (team in department in division). A check that walks ten levels makes ten sequential reads.
| Option | How | Cost | Verdict |
|---|---|---|---|
| A. Recursive walk | Follow group#member usersets at check time | Latency grows with depth | Default for shallow namespaces |
| B. Flattened membership index | An index builder consumes the changelog and maintains (group, transitive member) pairs; checks do one lookup | Write amplification when a large group changes; index lag | Chosen for group namespace |
| C. Restrict nesting | Forbid nested groups | Product limitation | Rejected |
Choice and reason. The index is a set computation over the changelog, maintained incrementally and versioned by commit timestamp. A check that needs a snapshot newer than the index falls back to option A for the missing interval, so correctness never depends on index lag. Measure write amplification: adding a group of 50 000 members to a parent group creates up to 50 000 index entries.
7.4 Deep dive: reverse lookups for lists and search
| Option | How | Cost | Verdict |
|---|---|---|---|
| A. Check each candidate | Product fetches candidates, then BatchCheck | Wasted work when most candidates are denied | For pages of at most [100] items |
| B. ListObjects | Service walks the graph backwards from the subject | Expensive for subjects in many groups; must be bounded | Chosen for small result sets |
| C. Pre-filter in search index | Search documents carry the set of principals allowed to view; queries filter by the user's groups; final page is re-checked | Index lag; ACL changes reindex documents | Chosen for full-text search |
Choice and reason. Combine C with a final BatchCheck of the returned
page so that index lag can hide a result but never leak one.
7.5 Schema changes
Treat the namespace configuration like a database migration: validate types, reject removal of a relation that still has tuples, run the new version in shadow mode (evaluate both versions and compare results on sampled traffic), then activate. Every check response reports the schema version used.
How real systems do it: Google Zanzibar
The Zanzibar paper (Pang et al., USENIX ATC 2019) describes a global system that stores access control lists as relation tuples in Spanner and evaluates them with namespace configurations made of userset rewrite rules. Its consistency token, called a zookie, encodes a commit timestamp; clients receive one when they change content and send it back with later checks, which is how Zanzibar addresses the new-enemy problem while still serving most checks from slightly older snapshots. The paper describes forwarding sub-requests to peers chosen by consistent hashing so caches are shared, a lock table that merges identical concurrent requests, request hedging against slow replicas, and a separate indexing system (Leopard) that flattens deeply nested group membership. The authors report 95th-percentile latency under 10 ms and availability above 99.999% over three years of production use. This box is a paraphrase; read the paper for the actual architecture and measurements.
8. Scalability and performance
| Concern | Approach | Limit before redesign |
|---|---|---|
| Check rate | Stateless servers, distributed cache | Example: Cache memory per region |
| Hot objects | Owner deduplication, quantized snapshots | One owner server's CPU per key |
| Storage reads | 32 shards, hedged reads | Example: 640 000 reads/s at 20 000 per shard |
| Write rate | Sharded commits, batched changelog | Cross-shard writes needing coordination |
| Nested groups | Flattened index | Index rebuild time after a large group change |
| ListObjects | Bounded results, pre-filter for search | Subjects in more than [10 000] groups |
Latency budget for an uncached check at p95 (target 10 ms):
| Step | Budget |
|---|---|
| Parse, authenticate caller, pick snapshot | 0.5 ms |
| Forward to cache owner (in-cluster hop) | 1.0 ms |
| Up to two parallel storage read rounds | 5.0 ms |
| Group index lookup | 1.5 ms |
| Headroom | 2.0 ms |
9. Reliability, security and operations
9.1 Failure modes
| Component | Failure | Detection | Blast radius | Mitigation | RTO |
|---|---|---|---|---|---|
| Check servers | Example: Region fleet unavailable | Health checks, error rate | All products in region | Route to nearest region; clients fail closed | 5 min |
| Tuple store shard | Example: Leader loss | Store metrics, read errors on shard | Objects on that shard | Automatic leader election; reads from replicas at older snapshots where tokens allow | 1 min |
| Result cache | Example: Fleet restart empties caches | Hit ratio below 0.6 | Storage load doubles | Gradual rollout; warm from peers; admission control | 15 min |
| Group index | Example: Builder stalls | Index lag above 30 s | Nested-group checks slower | Fall back to recursive walk (7.3) | 30 min |
| Schema rollout | Example: Bad rewrite rule activated | Shadow diff, deny-rate spike | Every check in the namespace | Shadow mode; one-step rollback to previous version | 5 min |
| Client | Example: Product drops tokens | Share of checks without token | New-enemy exposure in that product | Client library defaults; conformance test | n/a |
| [Component] | [Failure] | [Signal] | [Scope] | [Mitigation] | [RTO] |
Fail closed. When a check cannot complete within its deadline, the client library denies. Clients may cache positive results for at most [5] seconds to ride out short blips; they never cache across a permission write they made themselves, and never answer a check that carries a token newer than the cached result's snapshot from that cache.
9.2 Security
| Threat | Control |
|---|---|
| Product service writes tuples outside its namespaces | Per-caller authorization on Write by namespace, enforced by the service |
| Privilege escalation through schema change | Schema changes require review by the schema owner and security; audited |
| Enumeration via ListSubjects | Restricted to callers with an admin scope |
| Tuple data reveals org structure | Encrypt at rest; tuples are confidential data |
| Denial of service by deep or wide graphs | Depth limit, fan-out limit, per-tenant quotas |
| [Threat] | [Control] |
Cross-check with the Threat Model Template (security/threat-model) and the
policy decision point role in the Zero Trust Architecture Plan Template
(security/zero-trust-architecture).
9.3 Observability
| Signal | Metric | Alert threshold |
|---|---|---|
| Check latency | p95 and p99 per API and region | Example: p95 above 10 ms for 10 min |
| Cache hit ratio | Per region | Example: below 0.7 for 15 min |
| Snapshot age served | Distribution of (now minus snapshot) | Example: p99 above 10 s |
| Token usage | Share of checks with a token, per product | Drop of 20% vs. 7-day baseline |
| Depth and fan-out | p99 sub-requests per check | Above 50 |
| Watch lag | Oldest unconsumed change per subscriber | Above 60 s |
9.4 Cost estimate
| Item | Unit cost | Quantity | Monthly cost |
|---|---|---|---|
| Check servers | [USD per instance-month] | Example: 60 per region x 3 | [USD] |
| Tuple store | [USD per node-month and per TB] | Example: 32 shards x 3 replicas, 6 TB | [USD] |
| Group index store | [USD] | [Size] | [USD] |
| Cross-region replication traffic | [USD per GB] | [GB] | [USD] |
| Total | [USD] |
10. Trade-offs and alternatives
| Decision | Options | Chosen | Why | What we give up |
|---|---|---|---|---|
| Model | Roles in each app; attribute policies; relationship tuples | Relationship tuples | Sharing, groups and hierarchy expressed as data | Learning curve; attributes need caveats later |
| Consistency | Full; bounded staleness; tokens | Tokens | Correct where it matters, cached elsewhere | Clients must store tokens |
| Store | Key-value store; sharded SQL; distributed SQL with external consistency | Distributed SQL | Snapshot reads at a timestamp across shards | Cost and operational expertise |
| Build or adopt | Build; adopt OpenFGA or SpiceDB; managed service | [Decision] | [Reason] | [Cost] |
| Lists | Check each; ListObjects; search pre-filter | Hybrid by result size | Bounded cost per call | Two code paths |
| At 10x (5 million checks/s) | Cache hit ratio and hot-key deduplication become the whole design; consider per-region read replicas of the group index and stricter fan-out limits | |||
| At 0.1x (50 000 checks/s) | A single-region managed database plus an open-source engine is enough; drop the group index and distributed cache |
11. Delivery plan and risks
11.1 Milestones
| Milestone | Scope | Exit criterion | Target date |
|---|---|---|---|
| M1 | Schema for documents and groups; Check and Write in one region | Conformance tests pass, including new-enemy tests | YYYY-MM-DD |
| M2 | Dual-run: products check both old logic and service, log differences | Difference rate below 0.01% for 14 days | YYYY-MM-DD |
| M3 | Service authoritative for documents | No P1 permission incidents for 30 days | YYYY-MM-DD |
| M4 | ListObjects and search pre-filter | Search leakage test passes | YYYY-MM-DD |
| M5 | Multi-region | Regional failover drill within RTO | YYYY-MM-DD |
11.2 Migration and rollback
[Example: A backfill job converts existing ACL rows into tuples; a
change-data-capture stream keeps them in sync during dual-run. The product
flag authz.source switches between legacy and service per namespace;
rollback flips the flag and takes effect within 60 s.]
11.3 Risks
| ID | Risk | Likelihood | Impact | Mitigation | Owner |
|---|---|---|---|---|---|
| R-01 | Example: Products ignore tokens, reopening the new-enemy problem | High | High | Client library stores and forwards tokens by default | [@name] |
| R-02 | Example: Fan-out of one schema rule breaks latency | Medium | High | Fan-out budget per rule in schema review | [@name] |
| R-03 | Example: Backfill misses legacy edge cases | Medium | High | Dual-run comparison before cut-over | [@name] |
| R-04 | Example: Service becomes a single point of failure for all products | Medium | High | Multi-region, fail-closed client behaviour tested in game days | [@name] |
| [R-nn] | [Risk] | [L / M / H] | [L / M / H] | [Mitigation] | [@name] |
12. Appendices
12.1 Traceability
| Requirement | Design element | Verification |
|---|---|---|
| FR-01, NFR-01 | 7.2 caching, 8 latency budget | Load test |
| FR-06, NFR-03 | 7.1 tokens | Consistency test with injected replica lag |
| FR-05 | 5.3 parent->view | Schema conformance test |
| FR-07 | 7.4 | Leakage test against search |
| FR-10 | 7.5 | Shadow diff report |
12.2 Open questions
| ID | Question | Owner | Due |
|---|---|---|---|
| OQ-01 | Example: Do we need attribute conditions (caveats) in phase 1? | [@name] | YYYY-MM-DD |
| OQ-02 | Example: How long do we retain snapshots for at_exact_snapshot paging? | [@name] | YYYY-MM-DD |
| [OQ-nn] | [Question] | [@name] | YYYY-MM-DD |
12.3 Glossary
| Term | Meaning |
|---|---|
| Relation tuple | A stored fact object#relation@subject |
| Userset | A set of subjects defined as "everyone with relation R on object O" |
| Rewrite rule | A rule computing one relation from others (union, intersection, exclusion, tuple-to-userset) |
| Consistency token (zookie) | Opaque value encoding a snapshot timestamp, returned by writes and accepted by reads |
| New-enemy problem | A check evaluated against permissions older than the content it protects |
| ReBAC | Relationship-based access control |
| Fan-out | Number of sub-checks or reads one check causes |
12.4 References
| Reference | Use |
|---|---|
| Pang et al., "Zanzibar: Google's Consistent, Global Authorization System", USENIX ATC 2019, pp. 33 to 46 | Model, tokens, caching, indexing; 7.5 box |
| OpenFGA documentation (openfga.dev), consistency modes and query APIs | Check, Expand, ListObjects, ListUsers; consistency options |
| SpiceDB documentation (authzed.com/docs), consistency and ZedTokens | Consistency modes in 4.1 |
| RFC 9457 Problem Details for HTTP APIs | Error bodies |
System Design Document Template (system-design/system-design-document) | Parent structure |
Distributed Key-Value Store System Design Template (system-design/design-distributed-kv-store) | Storage trade-offs |
Threat Model Template (security/threat-model) | Threat analysis of this service |
Standard and licence note
Based on the public Zanzibar paper and the public documentation of the OpenFGA and SpiceDB projects cited in 12.4, and on common industry practice for authorization services. This is a reference design with illustrative numbers, not a description of any vendor's product; check the current documentation of the engine you adopt, because consistency options and API names differ between engines and versions. Original text written for this collection. Template licence: MIT.
関連テンプレート
Notification Service System Design Template (Reference Design)
Push, email, SMS and in-app: templates, preferences and opt-out, priority queues, caps, provider failover, retries, dedup
システム設計CDC Pipeline System Design Template
CDC pipeline: log-based capture, watermark chunked snapshot, per-key order, schema evolution, outbox, search, cache and warehouse sinks, replay
システム設計Chat System Design Template (Reference Design)
Real-time 1:1 and group chat: WebSocket gateways, per-conversation ordering, fan-out, receipts, offline sync, push, E2EE