One Number to Rule the Code: Bitmasking That Scales the Load by Hardik Raja on August 25, 2025 119 views

The client asked for a new role type with some custom permissions.
I opened the
PermissionFlag
enum, added a few lines, pushed the code…and went to grab a coffee. ☕
The change took a minute. Literally.
It wasn’t always this simple. What now takes minutes used to take days—modifying the database, adjusting backend logic, and updating the UI for even the smallest change. Each new role or action added layers of complexity.
But with the bitmask pattern, everything changed.
Bitmask is a number where each bit (0 or 1) shows if something is on or off. This way, one number can keep track of many true/false values at once. That’s bitmasking in a nutshell.
This lightweight approach has not only simplified permission handling but opened the door to flexible, scalable design across the system. And this isn’t just about backend tweaks or permission models—it’s about building systems that scale effortlessly. As applications grow, so do their needs. More roles, more toggles, more boolean flags—until your data model becomes bloated and brittle.
Bitmasking helped us move from a forest of boolean fields to a single, dynamic value. It’s a technique that’s:
- Simple to implement
- Easy to maintain
- Clean and efficient across frontend and backend
We’ll walk through this with a real-world example of role-based permissions—but the takeaway is much broader: this is a strategy for designing systems that embrace change without breaking down.
The Pain of Managing Permissions with Booleans
Imagine a hotel management app with menus like Guest Check-In, Billing, and Housekeeping. The owner needs precise control over actions on each menu based on different role types. At the foundation, the system defines three primary role types:
- Manager
- Assistant Manager
- Helper
Under these, specific roles will be created in the future. Each menu of the application allows users to perform three possible actions: Create record, Edit record, and View record.
To support every possible configuration for each menu, the system initially used nine boolean columns in the menu
table—one for each (RoleType × Action) combination:
manager_create
,manager_edit
,manager_view
assistant_manager_create
,assistant_manager_edit
,assistant_manager_view
helper_create
,helper_edit
,helper_view
Now, if the hotel owner wants to configure the Guest Check-In menu such that:
- Manager role type has create access
- Assistant Manager role type has edit access
- Helper role type has view access
When a role is granted create access, the system automatically includes edit and view as underlying actions—and similarly, granting edit also implies view access.
Then the corresponding row in the menu
table might look like this:.
menu_name | manager_create | assistant_manager_edit | helper_view | Remaining six actions |
---|---|---|---|---|
Guest Check-In | true | true | true | false |
This hardcoded structure may work well when the set of role types is fixed. But the moment new role types are introduced or existing ones evolve, it turns into a feature development task rather than a simple configuration update. Each change in role type meant:
- Schema modifications and database migrations
- Code updates across entities/models and service layers
- Adjustments to validation logic
- And extensive rounds of testing and regression checks
So the real question became—how can we design it in a way that a simple change feels like magic and just works across the system?
Bitmasks to the Rescue
What we really needed was a way to store configuration in the database—without hardcoding role types across multiple columns.
Ideally, a single value should capture everything. And since each permission is essentially a yes or no, the answer was obvious: use a binary number.
How Bitmasking Works: A Practical Walkthrough
Before we jump into the example, let’s understand how the bitmask actually works.
Each permission—defined as a combination of RoleType × Action—is assigned a unique bit index. These bits are aligned in reverse order, with index 0 representing the rightmost bit in the binary string.
Here’s how all possible combinations map to bit positions:
Bit Index | RoleType | Action |
---|---|---|
0 | Manager | Create |
1 | Manager | Edit |
2 | Manager | View |
3 | Assistant Manager | Create |
4 | Assistant Manager | Edit |
5 | Assistant Manager | View |
6 | Helper | Create |
7 | Helper | Edit |
8 | Helper | View |
So, a binary string like:
⇒ 000000011
Would mean:
- Bit 0 (Manager–Create) = ✅
- Bit 1 (Manager–Edit) = ✅
- All others = ❌
Now, let’s say the hotel owner wants to configure the Guest Check-In menu with the following permissions for role type:
- Manager can create new guest entries.
- Assistant Manager can edit existing entries.
- Helper can view entries, but not create or edit.
We assign each (RoleType × Action) to a specific bit position, from 0 (rightmost) to 8 (leftmost):
Bit Position | RoleType–Action |
---|---|
0 | Manager–Create |
4 | AssistantManager–Edit |
8 | Helper–View |
Setting these three bits gives us the binary mask:
Position: 8 7 6 5 4 3 2 1 0 Bits: 1 0 0 0 1 0 0 0 1
Reading 100010001
as a decimal number yields 273 (256 + 16 + 1). This single integer value encodes all three permissions:
- Bit 0 → Manager–Create (1)
- Bit 4 → AssistantManager–Edit (16)
- Bit 8 → Helper–View (256)
By storing 273 in the permission_mask
column of menu
, we know exactly which RoleTypes can perform which actions.
Effortless Scalability: Adding New Roles Without Breaking Anything
Tomorrow, the hotel owner might introduce a new RoleType—Supervisor—with Create access to the Guest Check-In menu.
In the bitmask model, each permission maps to a fixed bit position, starting from the right (bit 0). To ensure compatibility with existing data, new permissions are always added to the left, preserving all previous mappings.
So when Supervisor is added, its actions—Create, Edit, and View—are assigned to bits 9, 10, and 11.
Initially, the system used a 9-bit binary number to represent permissions:
000000000
When a new RoleType like Supervisor is added, its three permissions (Create, Edit, View) are prepended to the existing bitmask, not appended. This expands the binary representation to 12 bits, like so:
xxx000000000
To support the requirement where the Supervisor RoleType can have Create access to the Guest Check-In Menu.
- The current permission mask of Guest Check-In menu is 273 (binary
100010001
) - Supervisor–Create maps to bit 9, which equals 512 (binary
001xxxxxxxxx
) - The updated mask becomes 273 + 512 = 785 (binary
1100010001
)
This approach guarantees complete backward compatibility.
Any menu entry with the existing value 273 will continue to function exactly as before. And whenever the updated value 785 is applied—such as for the Guest Check-In menu—Supervisor’s Create permission is seamlessly recognized.
No breakage. No rewrites. Just smooth, controlled evolution.
The Single Source Of Truth
Now, the real power lies in how this logic flows through the code.
If you look closely, everything in this approach is flexible and lightweight—except for one crucial anchor: bit index binding. Each permission relies on its position in the binary sequence, which means we need a consistent reference for those indexes.
This binding must be defined somewhere—either in the database or in code. For our case, we chose to manage it cleanly through an enum.
And here’s the trick: the ordinal of each enum constant becomes its index in the binary string. That means:
- The first constant (
ordinal = 0
) is bit 0 (rightmost) - The second is bit 1, and so on
public enum PermissionFlag {
MANAGER_CREATE,
MANAGER_EDIT,
MANAGER_VIEW,
ASSISTANT_MANAGER_CREATE,
ASSISTANT_MANAGER_EDIT,
ASSISTANT_MANAGER_VIEW,
HELPER_CREATE,
HELPER_EDIT,
HELPER_VIEW;
}
This enum becomes the single source of truth. It drives everything—from populating permissions on the screen to validating them when data is submitted.
Let’s walk through how it works.
When the hotel owner assigns permissions to a Manager role for various menus, the backend uses the permission_mask
—a decimal value stored in the database. This number is converted to binary, and each bit maps to a specific combination defined in the enum.
For example, when an Hotel Owner assigns permissions to a some manager type Role, the UI can dynamically load all enum values, display them as checkboxes.
Using this mapping, the UI automatically renders permissions as enabled, disabled. For example:
Menu | Create | Edit | View |
---|---|---|---|
Payment Gateway Settings | 🚫 | 🚫 | 🚫 |
Email Settings | 🚫 | 🚫 | 🚫 |
Guest Check-In | ⬜ | ⬜ | ⬜ |
Restaurant Table Booking | 🚫 | ⬜ | ⬜ |
- 🚫 (Disabled): Not applicable for this role type
- ⬜ (Unchecked Checkbox): Can be checked by the hotel owner, as permissions are granted using a decimal number.
So in this case:
- Email Settings and Payment Gateway Settings are completely disabled for the Manager—these menus are tied to a different RoleType (like Owner).
- For Guest Check-In, Create, Edit and View permission is accessible.
- In Restaurant Table Booking, Edit and View is granted for manager role type.
A Design That Scales: SOLID Principles in Action
This approach doesn’t just make things flexible—it follows strong architectural principles too. Most notably, it adheres to the Single Responsibility Principle and the Open/Closed Principle from the SOLID design model:
- Single Responsibility Principle (SRP): All permission logic lives in one place—
PermissionFlag
. No scattered conditionals, no duplicated logic. - Open/Closed Principle (OCP): The system is open for extension (you can add new RoleTypes and actions), but closed for modification (existing data and logic don’t need to be changed).
Your enum acts as the abstraction layer, the binary mask acts as the data model, and all permission behavior aligns with clean, scalable code.
Bitmask Limits and Data Type Choices
As powerful as bitmasks are, the number of bits you can store depends on the data type used. Here’s how to choose wisely:
- Use
int
(32 bits) when your system has 32 or fewer permissions. - Switch to
long
(64 bits) if you expect up to 64 combinations. - For cases with more than 64 permissions, use
BigInteger
in Java. If your database or target language doesn’t support large integers, consider storing the binary string representation instead.
This keeps your system future-ready, without risking overflow or data loss as new RoleTypes or actions are introduced.
Beyond Permissions: A Reusable Pattern for Boolean Explosion
This story was about role-based permissions—but the core idea extends far beyond that. Bitmasking is a versatile pattern you can use anytime you’re dealing with multiple yes/no flags that grow over time.
If your system has ever faced a situation like:
- A growing list of feature toggles
- Multiple capabilities assigned to users, devices, or plans
- Tracking status combinations (e.g., active, locked, pending approval, archived)
- Logging or storing event flags compactly
- Managing access rules in IoT systems, game levels, hardware capabilities
Then bitmasking can give you a clean, scalable way to manage it.
Instead of adding more columns or fields, you maintain a single compact integer and decode it using well-structured logic—often through enums or constants.
🏁 Final Thoughts : Think in Bits, Not Columns
Designing systems that scale effortlessly is not just about choosing the right tech—it’s about recognizing patterns. Bitmasking is one of those patterns that hides in plain sight but unlocks enormous power once embraced.
So next time you’re tempted to add that tenth boolean column… pause—and think in bits. 😉
🔗 Explore the Code
Curious to see this pattern in action?
Check out the working proof of concept (POC) on GitHub:
📂 Repository: github.com/Hardikraja/bitmask-pattern
This is a simple terminal-based Java demo that illustrates how to implement and manage permissions using bitmasking.
Feel free to explore, experiment, and adapt it to your needs!
Happy Coding!