Role-Based Access Control (RBAC) in a monolith application isn’t about assigning permissions to every single user; it’s about assigning permissions to roles, and then assigning users to those roles.

Let’s see this in action. Imagine a simple e-commerce monolith. We have users who can be customers or administrators.

// User Table
{
  "user_id": 1,
  "username": "alice",
  "role_id": 1 // Customer
}

{
  "user_id": 2,
  "username": "bob",
  "role_id": 2 // Administrator
}

// Role Table
{
  "role_id": 1,
  "role_name": "Customer"
}

{
  "role_id": 2,
  "role_name": "Administrator"
}

// Permission Table
{
  "permission_id": 1,
  "permission_name": "view_products"
}
{
  "permission_id": 2,
  "permission_name": "place_order"
}
{
  "permission_id": 3,
  "permission_name": "manage_products"
}
{
  "permission_id": 4,
  "permission_name": "view_users"
}

// Role_Permission Mapping Table
{
  "role_id": 1, // Customer
  "permission_id": 1 // view_products
}
{
  "role_id": 1, // Customer
  "permission_id": 2 // place_order
}
{
  "role_id": 2, // Administrator
  "permission_id": 1 // view_products
}
{
  "role_id": 2, // Administrator
  "permission_id": 3 // manage_products
}
{
  "role_id": 2, // Administrator
  "permission_id": 4 // view_users
}

When "alice" (user_id: 1) tries to place_order, the application checks:

  1. "alice" has role_id: 1 (Customer).
  2. The Customer role is associated with permission_id: 2 (place_order).
  3. Access granted.

When "bob" (user_id: 2) tries to manage_products, the application checks:

  1. "bob" has role_id: 2 (Administrator).
  2. The Administrator role is associated with permission_id: 3 (manage_products).
  3. Access granted.

If "alice" tried to manage_products, the check would fail at step 2 because the Customer role has no mapping to permission_id: 3.

The core problem RBAC solves is managing complex permission structures without needing to define unique permissions for every individual user. Instead of tracking hundreds or thousands of individual user-to-permission links, you manage a much smaller set of roles and the permissions they grant. Users are then simply members of these roles. This significantly reduces the administrative overhead and the potential for misconfiguration.

Internally, when a user action is requested, the application typically performs a lookup. It finds the user’s assigned role(s) and then checks if any of those roles have the specific permission required for the action. This often involves joining a few tables: users to roles to role_permissions to permissions. The role_permissions table acts as the crucial many-to-many intermediary between roles and permissions.

The exact levers you control are the definitions of your roles and the granular permissions available within your application. You define what a "Customer" can do (e.g., view_products, place_order) and what an "Administrator" can do (e.g., manage_products, view_users). The system then enforces these boundaries. You can create new roles (e.g., "Moderator," "Support Agent") and assign existing or new permissions to them, and then assign users to these new roles, all without modifying individual user records.

The surprising part is how rarely explicit user-to-permission assignments are needed in a well-designed RBAC monolith. Most of the time, an administrator isn’t granting "Bob" permission to delete products; they’re ensuring the "Administrator" role has that permission, and Bob is an administrator. The abstraction is key, and it often means you’re only ever looking at the permissions attached to a user’s role(s), not the user directly.

The next challenge arises when you need to grant a permission to a single user that deviates from their role, leading to the concept of permission overrides or exceptions.

Want structured learning?

Take the full Monolith course →