A staff engineer once described our authorization layer to me as "a very clever pile of if statements". He was not being kind. The system worked, mostly, but every new permission check was a 50-line decision tree, every code review on auth was a fresh argument, and every bug we shipped in that area landed on the front page of a customer's escalation ticket. We needed a model. The question was which one.
Authorization is the job of deciding "can subject S do action A on resource R?". RBAC, ABAC, and ReBAC are three different ways to organize that decision. They are not competitors in the way "REST vs GraphQL" is; they are each better at modeling different shapes of access policy. My stance: pick by the shape of your access rules, not by the model with the loudest documentation. RBAC for simple flat hierarchies, ABAC for attribute-driven rules where context matters, ReBAC for anything that involves social-graph-like relationships (sharing, collaboration, hierarchies of ownership).
The terminology pivot that always trips me up
Before we go further, the labels deserve unpacking, because "role" and "attribute" and "relationship" sound similar enough that people use them interchangeably and end up mixing models without realizing.
RBAC (Role-Based Access Control): permissions are assigned to roles, and users are assigned roles. "Editors can update articles" is a role-permission rule. "Alice is an editor" is a role assignment. The decision algorithm is: look up the user's roles, look up the permissions for those roles, allow if the action is in that set.
ABAC (Attribute-Based Access Control): the decision is a function of attributes of the subject, the resource, the action, and the environment. "Allow if the subject's department equals the resource's owner_department, and the time is within business hours, and the request comes from a corporate IP" is ABAC. The policies are usually expressed as rules over attributes, often in a dedicated policy language (XACML historically, Rego / OPA / Cedar today).
ReBAC (Relationship-Based Access Control): the decision is a function of the graph of relationships between subjects and resources. "Alice can read this document because Alice is in a group, the group is shared into a folder, the folder contains the document, and the sharing was set to read-only" is ReBAC. The decision algorithm is graph traversal: find a path from the subject to the resource that grants the action.
I find it useful to remember the shape of each model: RBAC is two tables (user_roles, role_permissions). ABAC is a rule engine over attributes. ReBAC is a graph database with permission edges. The shape of your data determines which model fits.
RBAC: the right default for most teams
Most products start with RBAC, and most products should. The model is simple, the implementation fits in a relational database, and the mental model is one most engineers already have.
The minimal data model:
A check is one join:
For the first 80% of products, this is enough. Roles map to job functions (admin, editor, viewer, billing-manager). Permissions map to actions on resources (articles:write, invoices:read, users:invite). The decision algorithm is fast, the data model is auditable, and the policy lives in your database where everything else is.
Where RBAC starts to crack:
- Per-resource permissions. "Alice is an editor on the Marketing project but only a viewer on the Product project." Now you need roles bound to a (user, resource) pair, not a (user) alone. You can squeeze this into RBAC by treating each project as having its own role assignments, but the model is straining.
- Conditional permissions. "Editors can publish articles, but only after 9am, and not for articles flagged as sensitive." Roles cannot express this; the conditions are about attributes (time of day, sensitivity flag) that are not roles.
- Permission explosion. Every new resource type adds new permissions; every new role adds new mappings; the matrix grows quadratically and no one wants to audit it.
When two of those three pressure points show up, the team is starting to outgrow RBAC.
ABAC: when context decides the answer
ABAC steps in when the rule needs to consider attributes of the subject, the resource, the action, or the environment. The classic ABAC policy looks like this in pseudocode:
The current ABAC stack of choice in most places I have seen is OPA (Open Policy Agent) with Rego policies, or AWS's Cedar language. Both let you write declarative rules over attribute trees, evaluate them against the actual request, and return allow/deny with a reason.
A small Cedar example:
The advantages: rules are explicit, attributes can come from anywhere (request context, identity provider claims, resource metadata), and policies can be unit-tested as code. The disadvantages: every check requires evaluating the rule against the current attributes, which means either a policy engine call (added latency) or pushing the policy down to the data layer (which is where most ABAC tools land).
Where ABAC starts to crack: when the rules need to consider relationships ("can Alice see this document?") rather than attributes ("is Alice's department equal to the document's owner_department?"). Relationships are not attributes. You can encode them as attributes (document.shared_with_users containing a list of user ids) but that quickly stops scaling, both in terms of performance and in terms of the rules being readable.
ReBAC: when the graph is the answer
ReBAC was, until a few years ago, a model people implemented ad hoc per product. Google formalized it internally as Zanzibar (the system behind Drive sharing, YouTube comments, and most of their authorization). The 2019 Zanzibar paper kicked off open-source implementations: SpiceDB, Ory Keto, OpenFGA, Permify. They are all reimplementations of the same fundamental idea.
The data model is a list of tuples:
The check question is: "is there a path from user:alice to document:roadmap via the reader relation, possibly through groups and folders?". The schema defines which relations imply which (e.g., "the parent folder's readers are also readers of the document; the group's members get the group's permissions").
A SpiceDB schema for this might look like:
Now "can Alice read the roadmap?" is a graph traversal: start at user:alice, find tuples granting access to document:roadmap directly or through groups/folders, walk the implied permissions. Zanzibar-style systems do this in a few milliseconds even at very large scale.
Where ReBAC shines: anything that looks like sharing, hierarchies, ownership, collaboration, parent-child resources, or social-graph-style access. Document sharing (Google Drive, Notion). Repository access (GitHub teams and orgs). Workspace membership (Slack, Linear). Family-tree style data (a parent account that owns child accounts). All of these have rules that are awkward to express in RBAC or ABAC and natural in ReBAC.
Where ReBAC starts to crack: when the rules are not really about relationships but about attributes (time of day, request IP, classification level). Bolting these onto a ReBAC model is possible but feels forced; you end up with synthetic relations like "user is currently authenticated with MFA" that drift the model away from its strength.
The decision shape I use
When I need to pick a model for a new system, I run through three questions:
- Are the rules mostly about who-the-user-is (job function)? RBAC.
- Are the rules mostly about contextual conditions (attributes of the subject, resource, time, IP)? ABAC.
- Are the rules mostly about how-the-user-relates-to-the-resource (sharing, ownership, group membership, hierarchy)? ReBAC.
Most real systems are a blend, and the right answer is rarely "pure ABAC" or "pure ReBAC". The strong default I have shipped is RBAC at the coarse layer (do you have an admin role at all?) plus ReBAC for resource-level decisions (which specific documents can you see?), with a thin ABAC layer for the special cases (time-of-day rules, MFA gates).
Each layer's rule shape stays clean. RBAC is not asked to express things it cannot. ReBAC is not bolted with synthetic relations to express attributes. ABAC is reserved for the small set of rules that genuinely need attribute logic.
Four authz anti-patterns I review for
The first mistake is starting with ABAC because "it is the most flexible". Flexibility is a cost, not a feature, in this context. ABAC policies are easy to write and very hard to audit. I have seen teams ship a Rego policy that effectively let any logged-in user read any resource because of an attribute check that defaulted to "true on missing attribute". The bug had been in production for months before someone noticed.
The second mistake is forcing ReBAC onto rules that are not about relationships. "Admins can do anything" is RBAC. Modeling "admin" as a relation in a ReBAC system works, but the schema becomes harder to read and the audit trail gets murkier. Use the right tool for the right rule.
The third mistake is splitting authorization across the application code and a policy engine without a single source of truth. I have seen systems where the database had user_roles, the application had hard-coded checks, and OPA had a separate policy file, and all three were out of sync. The fix is to pick the source of truth (a database, a policy bundle, a Zanzibar-style tuple store) and make every check go through it.
The fourth mistake is forgetting about negative permissions and conflicts. RBAC has no built-in deny; if any role grants the permission, the user has it. ABAC and ReBAC need explicit conflict-resolution rules ("deny wins, except for explicit overrides"). Document the conflict policy on day one.
Latency and the bigger system
A practical note. The check itself is on the hot path of every request. Whatever model you pick has to be fast: a few milliseconds at most, ideally under one. Caching is the universal answer. Cache decisions per (user, resource, action) for a short window (30-60 seconds), invalidate on permission changes (a Redis pub/sub on role changes flips the cache; a tuple delete in ReBAC flips the cache). The cache TTL is also your "how stale can a decision be" budget; pick it deliberately.
For audit, log every deny and a sample of allows. The deny log is gold during incident response: "why was this user blocked?" gets a trace immediately. The allow sample helps you spot patterns of accidental over-permissive policy.
The model is the spine; the implementation is the muscle
Pick the model first. Implement second. The most common failure I see is teams jumping straight to "we will use OPA" or "we will use SpiceDB" without first deciding what shape their access rules actually have. Both tools are excellent. Both are wrong if your rules do not fit their model.
If you take one thing away, it is this: RBAC, ABAC, and ReBAC are different shapes for different rules. They are not stages of maturity, they are not progress steps, they are not "RBAC for beginners and ABAC when you are ready". They are answers to different questions about your access policy. Read the rules you are actually writing, decide which question they are mostly answering, and pick the model that matches. The implementation will follow, and you will avoid the staff-engineer-described pile of clever if statements that I worked under for too long.
