How I Structure RBAC in a Spring Boot Application
Role-Based Access Control (RBAC) is one of those features that looks simple at first.
You create a few roles:
- ADMIN
- USER
- MANAGER
Then you protect endpoints with something like:
@PreAuthorize("hasRole('ADMIN')")
And everything seems fine.
Until the application grows.
Suddenly, you have dozens of roles, hundreds of permissions, different API access requirements, and business rules that are becoming difficult to manage.
In this article, I want to share a simple and scalable way to structure RBAC in a Spring Boot application.
The goal is not to build the most complicated authorization system possible.
The goal is to build something that is:
- Easy to understand
- Easy to extend
- Easy to test
- Suitable for enterprise applications
The Basic Idea
Instead of directly assigning access rules everywhere in the code, I prefer to separate the authorization model into three layers:
User
↓
Role
↓
Permission
For example:
User: john
Role:
ADMIN
Permissions:
USER_READ
USER_CREATE
USER_UPDATE
USER_DELETE
Another user might have:
User: alice
Role:
MANAGER
Permissions:
USER_READ
REPORT_VIEW
The key idea is simple:
Users receive roles, and roles receive permissions.
This creates a flexible authorization model that can grow with the application.
1. Modeling the Domain
Let's start with the three core concepts.
User
A user can have one or more roles.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
private Set<Role> roles = new HashSet<>();
}
For example:
john
Roles:
ADMIN
MANAGER
This is useful in enterprise applications where a user may have multiple responsibilities.
2. Role
A role represents a collection of permissions.
@Entity
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(fetch = FetchType.EAGER)
private Set<Permission> permissions = new HashSet<>();
}
Typical roles might be:
ADMIN
MANAGER
USER
AUDITOR
The role itself does not define business logic.
Its purpose is to group permissions.
3. Permission
Permissions represent specific capabilities in the system.
@Entity
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
For example:
USER_READ
USER_CREATE
USER_UPDATE
USER_DELETE
REPORT_VIEW
REPORT_EXPORT
I prefer permissions that describe a clear business action.
A simple naming convention is:
RESOURCE_ACTION
For example:
USER_READ
USER_CREATE
USER_UPDATE
USER_DELETE
Or in an AI document application:
DOCUMENT_UPLOAD
DOCUMENT_DELETE
DOCUMENT_ANALYZE
4. Why Not Only Use Roles?
For small applications, this works perfectly well:
@PreAuthorize("hasRole('ADMIN')")
But as the application grows, you may end up with roles like:
ADMIN
SUPER_ADMIN
SUPPORT_ADMIN
FINANCE_ADMIN
CONTENT_ADMIN
REPORT_ADMIN
Then authorization rules can become tightly coupled to role names.
For example:
@PreAuthorize(
"hasRole('ADMIN') or hasRole('MANAGER') or hasRole('SUPPORT_ADMIN')"
)
This becomes difficult to maintain.
Instead, permissions allow us to express the actual capability required:
@PreAuthorize("hasAuthority('USER_DELETE')")
Now the code tells us exactly what permission is required.
That is an important distinction.
Roles may change over time.
Business capabilities are often more stable.
5. Converting Roles and Permissions into Authorities
Spring Security works with GrantedAuthority.
When loading a user, we can convert both roles and permissions into authorities.
For example:
public Collection<? extends GrantedAuthority> getAuthorities(User user) {
Set<GrantedAuthority> authorities = new HashSet<>();
for (Role role : user.getRoles()) {
authorities.add(
new SimpleGrantedAuthority(
"ROLE_" + role.getName()
)
);
for (Permission permission : role.getPermissions()) {
authorities.add(
new SimpleGrantedAuthority(
permission.getName()
)
);
}
}
return authorities;
}
The resulting authorities might look like:
ROLE_ADMIN
USER_READ
USER_CREATE
USER_UPDATE
USER_DELETE
Now Spring Security can check both roles and permissions.
6. Protecting API Endpoints
For broad access rules, roles can still be useful:
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/users")
public List<User> getUsers() {
return userService.findAll();
}
For more specific actions, I prefer permissions:
@PreAuthorize("hasAuthority('USER_DELETE')")
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
Another example:
@PreAuthorize("hasAuthority('DOCUMENT_ANALYZE')")
@PostMapping("/documents/{id}/analyze")
public AnalysisResult analyzeDocument(
@PathVariable Long id) {
return documentService.analyze(id);
}
The required capability is immediately clear.
7. Avoid Hardcoding Authorization Logic Everywhere
One pattern I try to avoid is spreading authorization rules throughout business logic.
For example:
if (user.hasRole("ADMIN")) {
// do something
}
Or:
if (currentUser.getRole().equals("MANAGER")) {
// allow access
}
Over time, these checks become difficult to track and maintain.
Instead, authorization should be centralized where possible.
For example:
@PreAuthorize("hasAuthority('REPORT_EXPORT')")
For more complex rules, a custom authorization service can be useful:
@PreAuthorize("@authorizationService.canAccessDocument(#documentId)")
For example, access might depend on:
Permission
+
Organization
+
Ownership
+
Business Rules
8. RBAC Is Only the First Layer
A useful mental model is:
Authentication
↓
Role
↓
Permission
↓
Business Context
For example:
- Is the user authenticated?
- Does the user have the required permission?
- Does the user belong to the correct organization?
- Does the user own or have access to the resource?
A user may have the permission:
DOCUMENT_DELETE
But they should still not necessarily be able to delete every document in the system.
They may only be allowed to delete documents belonging to their own organization.
This is where business-level authorization becomes important.
My Recommendation
For a new Spring Boot application, I recommend starting with:
User
↓
Role
↓
Permission
Use:
hasRole(...)
for broad access control, and:
hasAuthority(...)
for specific business capabilities.
As the application grows, add context-aware authorization when necessary.
For example:
Permission
+
Organization
+
Ownership
+
Business Rules
This approach keeps the authorization model simple while still allowing it to grow with the application.
Final Thoughts
RBAC is not just about creating an ADMIN role.
The real challenge is designing an authorization model that still makes sense when your application has:
- Multiple teams
- Multiple roles
- Hundreds of permissions
- Multi-tenant organizations
- Complex business rules
My approach is to start simple:
User → Role → Permission
Then add complexity only when the business actually requires it.
A good authorization system should be predictable, easy to understand, and easy to extend.
That is usually better than building the most sophisticated security architecture on day one.
What does your RBAC structure look like?
Do you prefer:
User → Role
or:
User → Role → Permission
I'd love to hear how other Spring Boot developers structure authorization in their applications.
I'm building open-source projects around Java, Spring Boot, enterprise application architecture, and AI-powered applications.
My goal is to share practical implementation ideas and reusable tools that can help developers build enterprise applications faster.
Top comments (0)