Role Pattern
Assign dynamic roles to objects at runtime instead of hard-coding behavior in class hierarchies, enabling flexible identity changes without inheritance bloat.
Overview
The Role Pattern attaches behavior to objects at runtime instead of baking it into the class hierarchy. The same entity often plays different parts depending on context: one person can be a customer, an employee, and a vendor — sometimes at once. Model that with inheritance and you end up subclassing every combination (CustomerEmployeePerson, VendorCustomerPerson…).
The fix is to split the core entity (who the object is) from its roles (what it does). Roles attach, detach, and combine at runtime, so the entity keeps one identity while its capabilities change.
You’ll find this pattern behind authorization systems (RBAC), workflow engines, and multi-tenant apps — anywhere identity and capability need to move independently.
When to Use
Use the Role Pattern when:
- One entity plays several roles depending on context
- Inheritance can’t cover the role combinations (customer + employee + vendor multiply subclasses)
- Roles get assigned, revoked, or changed at runtime
- Role assignments must be auditable (who had what role, when)
When to Avoid
- Roles are static and known at compile time (a simple type hierarchy suffices)
- The system has only one or two roles with no overlap
- Introducing role objects adds indirection without clear benefit
- Performance is critical and role lookup overhead is unacceptable
- For alternatives like shared-context coordination, see the Blackboard Pattern
Solution
Python
from typing import Dict, List, Optional, Protocol
from dataclasses import dataclass, field
from datetime import datetime
class Role(Protocol):
"""Protocol for roles that can be attached to entities"""
role_name: str
def can_perform(self, action: str) -> bool:
...
@dataclass
class CustomerRole:
role_name: str = "customer"
loyalty_points: int = 0
def can_perform(self, action: str) -> bool:
return action in ["browse", "purchase", "review"]
def earn_points(self, amount: int):
self.loyalty_points += amount
@dataclass
class EmployeeRole:
role_name: str = "employee"
department: str = ""
salary: float = 0.0
def can_perform(self, action: str) -> bool:
return action in ["browse", "inventory", "support", "refund"]
def process_refund(self, order_id: str) -> str:
return f"Refund processed for {order_id}"
@dataclass
class VendorRole:
role_name: str = "vendor"
company_name: str = ""
commission_rate: float = 0.05
def can_perform(self, action: str) -> bool:
return action in ["list_products", "manage_inventory", "view_sales"]
class Person:
"""Core entity that can hold multiple roles"""
def __init__(self, person_id: str, name: str):
self.person_id = person_id
self.name = name
self._roles: Dict[str, Role] = {}
self._role_history: List[dict] = []
def assign_role(self, role: Role):
self._roles[role.role_name] = role
self._role_history.append({
"action": "assigned",
"role": role.role_name,
"timestamp": datetime.now().isoformat()
})
def revoke_role(self, role_name: str):
if role_name in self._roles:
del self._roles[role_name]
self._role_history.append({
"action": "revoked",
"role": role_name,
"timestamp": datetime.now().isoformat()
})
def has_role(self, role_name: str) -> bool:
return role_name in self._roles
def get_role(self, role_name: str) -> Optional[Role]:
return self._roles.get(role_name)
def can_perform(self, action: str) -> bool:
return any(role.can_perform(action) for role in self._roles.values())
@property
def roles(self) -> List[str]:
return list(self._roles.keys())
# Usage
person = Person("P-001", "Alice")
person.assign_role(CustomerRole(loyalty_points=100))
person.assign_role(EmployeeRole(department="Sales", salary=75000))
print(f"Alice's roles: {person.roles}")
print(f"Can browse: {person.can_perform('browse')}")
print(f"Can refund: {person.can_perform('refund')}")
print(f"Can list products: {person.can_perform('list_products')}")
# Access role-specific behavior
emp_role = person.get_role("employee")
if emp_role:
print(emp_role.process_refund("ORD-123"))
# Revoke a role
person.revoke_role("customer")
print(f"Roles after revoke: {person.roles}")
Java
import java.util.*;
interface Role {
String getRoleName();
boolean canPerform(String action);
}
class CustomerRole implements Role {
private int loyaltyPoints = 0;
public String getRoleName() { return "customer"; }
public boolean canPerform(String action) {
return List.of("browse", "purchase", "review").contains(action);
}
public void earnPoints(int amount) { loyaltyPoints += amount; }
}
class EmployeeRole implements Role {
private String department;
private double salary;
public EmployeeRole(String department, double salary) {
this.department = department; this.salary = salary;
}
public String getRoleName() { return "employee"; }
public boolean canPerform(String action) {
return List.of("browse", "inventory", "support", "refund").contains(action);
}
public String processRefund(String orderId) { return "Refund processed for " + orderId; }
}
class Person {
private final String personId;
private final String name;
private final Map<String, Role> roles = new HashMap<>();
private final List<Map<String, String>> history = new ArrayList<>();
public Person(String personId, String name) {
this.personId = personId; this.name = name;
}
public void assignRole(Role role) {
roles.put(role.getRoleName(), role);
history.add(Map.of("action", "assigned", "role", role.getRoleName()));
}
public void revokeRole(String roleName) {
if (roles.remove(roleName) != null) {
history.add(Map.of("action", "revoked", "role", roleName));
}
}
public boolean hasRole(String roleName) { return roles.containsKey(roleName); }
public Role getRole(String roleName) { return roles.get(roleName); }
public boolean canPerform(String action) {
return roles.values().stream().anyMatch(r -> r.canPerform(action));
}
public List<String> getRoles() { return new ArrayList<>(roles.keySet()); }
}
// Usage
Person person = new Person("P-001", "Alice");
person.assignRole(new CustomerRole());
person.assignRole(new EmployeeRole("Sales", 75000));
System.out.println("Roles: " + person.getRoles());
System.out.println("Can browse: " + person.canPerform("browse"));
System.out.println("Can refund: " + person.canPerform("refund"));
JavaScript
class CustomerRole {
constructor(loyaltyPoints = 0) {
this.roleName = 'customer';
this.loyaltyPoints = loyaltyPoints;
}
canPerform(action) {
return ['browse', 'purchase', 'review'].includes(action);
}
earnPoints(amount) {
this.loyaltyPoints += amount;
}
}
class EmployeeRole {
constructor(department, salary) {
this.roleName = 'employee';
this.department = department;
this.salary = salary;
}
canPerform(action) {
return ['browse', 'inventory', 'support', 'refund'].includes(action);
}
processRefund(orderId) {
return `Refund processed for ${orderId}`;
}
}
class VendorRole {
constructor(companyName, commissionRate = 0.05) {
this.roleName = 'vendor';
this.companyName = companyName;
this.commissionRate = commissionRate;
}
canPerform(action) {
return ['list_products', 'manage_inventory', 'view_sales'].includes(action);
}
}
class Person {
constructor(personId, name) {
this.personId = personId;
this.name = name;
this.roles = new Map();
this.history = [];
}
assignRole(role) {
this.roles.set(role.roleName, role);
this.history.push({ action: 'assigned', role: role.roleName, timestamp: new Date().toISOString() });
}
revokeRole(roleName) {
if (this.roles.has(roleName)) {
this.roles.delete(roleName);
this.history.push({ action: 'revoked', role: roleName, timestamp: new Date().toISOString() });
}
}
hasRole(roleName) {
return this.roles.has(roleName);
}
getRole(roleName) {
return this.roles.get(roleName);
}
canPerform(action) {
for (const role of this.roles.values()) {
if (role.canPerform(action)) return true;
}
return false;
}
getRoleNames() {
return Array.from(this.roles.keys());
}
}
// Usage
const person = new Person('P-001', 'Alice');
person.assignRole(new CustomerRole(100));
person.assignRole(new EmployeeRole('Sales', 75000));
console.log('Roles:', person.getRoleNames());
console.log('Can browse:', person.canPerform('browse'));
console.log('Can refund:', person.canPerform('refund'));
const empRole = person.getRole('employee');
if (empRole) console.log(empRole.processRefund('ORD-123'));
person.revokeRole('customer');
console.log('Roles after revoke:', person.getRoleNames());
Explanation
The pattern separates identity from behavior:
- Entity (Person): the stable identity — ID, name, nothing role-specific
- Role (CustomerRole, EmployeeRole): behavior attached and detached at runtime
- Role history: a log of assign/revoke events for auditing
Callers ask the entity; the entity asks its attached roles. Revoking a role removes its capabilities without touching the entity’s identity — no CustomerEmployeePerson extends Person chains anywhere.
Variants
| Variant | Mechanism | Use Case |
|---|---|---|
| Role Object | Separate role instances attached to entity | DDD, authorization |
| Trait/Mixin | Language-level composition | Scala traits, Ruby modules |
| RBAC | Roles with permissions matrix | Enterprise authorization |
| State Machine | Roles as states with transitions | Workflow engines |
What Works
- Keep the entity thin. The entity should only hold identity and role management.
- Make roles stateful when needed. A
CustomerRolecan track loyalty points. - Audit role changes. Security and compliance often require role transition logs.
- Avoid role overlap confusion. If two roles grant the same permission, document the resolution strategy.
- Use value objects for role data. Immutable role data shouldn’t be mutated directly.
Common Mistakes
- Using inheritance for roles.
Employee extends Persondoesn’t allow a person to also be a customer. - God entity. The entity class accumulates fields for every possible role.
- Role permission conflicts. Two roles grant conflicting permissions with no resolution rule.
- Not cleaning up on role revocation. Resources held by a role should be released when revoked.
- Storing role-specific data on the entity. Customer loyalty points belong in
CustomerRole, notPerson.
Real-World Examples
RBAC (Role-Based Access Control)
Enterprise systems assign users to roles (Admin, Editor, Viewer) rather than granting permissions directly — NIST standardized RBAC on exactly this model. The Role Pattern is the implementation basis underneath it.
DDD Aggregates
In Domain-Driven Design, a Person entity may hold Customer and Employee value objects as roles within a bounded context.
Game Characters
Game entities often have role components: WarriorRole, MerchantRole, QuestGiverRole that are attached to a base Character entity.
Companion code: the role-pattern companion repo packages all three implementations as runnable projects.
Frequently Asked Questions
What's the difference between Role and Strategy?
Strategy swaps how something is done (the algorithm). Role swaps what the object is allowed to be (identity plus capability). A person can hold several roles at once; a strategy is usually a single active choice.
Can an entity hold multiple instances of the same role?
Usually no — one employee role per person. Some domains do allow it (a person under two separate vendor contracts); if yours does, store roles in a list keyed by a qualifier instead of a plain map.
How does this relate to RBAC?
RBAC is the authorization application of this pattern: roles carry permissions, and the entity gains capabilities through its assigned roles. The pattern adds assign/revoke and history; RBAC adds the permission matrix on top.
What happens when two roles grant conflicting permissions?
The simple rule is union — any role granting the action allows it. Stricter domains need a resolution policy (deny-overrides, precedence order, or explicit conflict checks at assignment time). Pick one and write it down; silent conflicts become security bugs.
How do I persist roles and their history?
Store assignments as events (assigned, revoked, timestamp, actor) alongside the entity. Roles themselves can live in a join table or as serialized value objects on the entity row — keep them separate so revoking doesn't touch entity data.
Is this overkill for a small project?
Often, yes. Two roles with no overlap? Use plain composition or a flag. The pattern pays off when role combinations multiply or roles change at runtime — introduce it when the combinatorial pain shows up, not before.
Can I attach a role conditionally at runtime?
That's the point. Gate assignRole with whatever rule you need — purchase history, manual approval, a workflow step — and keep the check outside the role itself so the role stays a pure capability bundle.
Related Resources
Decorator Pattern
Add new functionality to objects dynamically by wrapping them. A structural design pattern for flexible behavior extension.
PatternStrategy Pattern
Define a family of algorithms, encapsulate each one, and make them interchangeable. A behavioral design pattern for flexible behavior selection.
PatternComposite Entity Pattern
Map a coarse-grained entity to multiple database tables by composing dependent objects, so the whole aggregate loads and saves as one unit.
PatternTwin Pattern
Link two classes through mutual references so they delegate methods to each other: a composition-based alternative to multiple inheritance.
PatternManager Pattern
Centralize object lifecycle, lookup, and cleanup behind a dedicated manager class — from resource pools to entity managers and service registries.