Using Bitfields for Permissions and Feature Flags
Introduction
Bitfields are a powerful technique for managing permissions and feature flags efficiently. I learned about bitfields since the beginning of my career back in late 2018, thanks to Discord's permissions documentation.
Instead of storing multiple boolean values or arrays, bitfields pack multiple flags into a single number using bitwise operations. This approach is memory-efficient, fast, and perfect for systems that need to check many permissions or features quickly.
How Bitfields Work
A bitfield uses individual bits in a number to represent different flags. Each bit position represents a specific permission or feature:
Bit Position | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Binary | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 1 |
In this example the decimal value 173
is represented as 10101101
in binary. Bits 0
, 2
, 3
, 5
, and 7
are set (1)
, while bits 1
, 4
, and 6
are unset (0)
.
Logic Gates and Bitwise Operations
Bitfields work using basic logic gates. Here's how each one works with truth tables:
AND (&) - Checks if a bit is set
The AND operation returns 1 only when both inputs are 1:
A | B | A & B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
OR (|) - Sets a bit
The OR operation returns 1 when at least one input is 1:
A | B | A | B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
XOR (^) - Toggles a bit
The XOR operation returns 1 when inputs are different:
A | B | A ^ B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
NOT (~) - Flips all bits
The NOT operation flips each bit (0 becomes 1, 1 becomes 0):
A | ~A |
---|---|
0 | 1 |
1 | 0 |
Programmatically you can use the following operations:
// Example with 8-bit number: 10101101 (173)
const value = 173n; // Binary: 10101101
// Check if bit 2 is set (AND operation)
const isBit2Set = (value & (1n << 2n)) !== 0n; // true
// Set bit 1 (OR operation)
const setBit1 = value | (1n << 1n); // 175n (10101111)
// Toggle bit 0 (XOR operation)
const toggleBit0 = value ^ (1n << 0n); // 172n (10101100)
// remove bit 2
const removeBit2 = value & ~(1n << 2n); // 169n (10101001)
Basic BitField Implementation
Here's a simple BitField class for managing permissions:
class BitField {
private value: bigint;
constructor(value: bigint | string | number = 0n) {
this.value = BigInt(value);
}
// Add a permission (set bit)
add(permission: bigint): this {
this.value |= (1n << permission);
return this;
}
// Remove a permission (clear bit)
remove(permission: bigint): this {
this.value &= ~(1n << permission);
return this;
}
// Toggle a permission (flip bit)
toggle(permission: bigint): this {
this.value ^= (1n << permission);
return this;
}
// Check if permission exists (check bit)
has(permission: bigint): boolean {
return (this.value & (1n << permission)) !== 0n;
}
// Check if all permissions exist
hasAll(permissions: bigint[]): boolean {
return permissions.every(permission => this.has(permission));
}
// Check if any permission exists
hasSome(permissions: bigint[]): boolean {
return permissions.some(permission => this.has(permission));
}
// Get all set permissions
toArray(): bigint[] {
const permissions: bigint[] = [];
let temp = this.value;
let position = 0n;
while (temp > 0n) {
if (temp & 1n) {
permissions.push(position);
}
temp >>= 1n;
position++;
}
return permissions;
}
// Serialize to string for storage/transfer
toString(): string {
return this.value.toString();
}
// Create from string
static fromString(value: string): BitField {
return new BitField(value);
}
}
Permission System Example
// Define permission constants using bit shifts
const PERMISSIONS = {
READ: 1n << 0n, // 1n
WRITE: 1n << 1n, // 2n
DELETE: 1n << 2n, // 4n
ADMIN: 1n << 3n, // 8n
MODERATE: 1n << 4n, // 16n
} as const;
// Create user permissions
const userPermissions = new BitField()
.add(PERMISSIONS.READ)
.add(PERMISSIONS.WRITE);
// Check permissions
console.log(userPermissions.has(PERMISSIONS.READ)); // true
console.log(userPermissions.has(PERMISSIONS.DELETE)); // false
// Add admin permission
userPermissions.add(PERMISSIONS.ADMIN);
// Check multiple permissions
console.log(userPermissions.hasAll([PERMISSIONS.READ, PERMISSIONS.WRITE])); // true
// Serialize for database storage
const serialized = userPermissions.toString(); // "11"
Feature Flags Example
const FEATURES = {
DARK_MODE: 1n << 0n,
PREMIUM_CONTENT: 1n << 1n,
BETA_FEATURES: 1n << 2n,
ANALYTICS: 1n << 3n,
NOTIFICATIONS: 1n << 4n,
} as const;
class User {
private features: BitField;
constructor(features: string = "0") {
this.features = BitField.fromString(features);
}
enableFeature(feature: bigint): void {
this.features.add(feature);
}
disableFeature(feature: bigint): void {
this.features.remove(feature);
}
hasFeature(feature: bigint): boolean {
return this.features.has(feature);
}
getFeatures(): string {
return this.features.toString();
}
}
// Usage
const user = new User();
user.enableFeature(FEATURES.DARK_MODE);
user.enableFeature(FEATURES.PREMIUM_CONTENT);
console.log(user.hasFeature(FEATURES.DARK_MODE)); // true
console.log(user.getFeatures()); // "3"
Advantages and Disadvantages
Advantages
Advantage | Description |
---|---|
Memory Efficient | Single number instead of multiple booleans |
Fast Operations | Bitwise operations are extremely fast |
Easy Serialization | Single value to store/transfer |
Atomic Operations | All flags updated together |
Disadvantages
Disadvantage | Description |
---|---|
Not Human Readable | Requires bit manipulation knowledge |
Debugging Difficulty | Hard to understand raw values |
Memory Usage | Can use significant memory with many flags |
When to Use Bitfields
Use bitfields when:
- You have many boolean flags (10+)
- Performance is critical
- You need atomic flag operations
- Memory usage matters
- Flags are frequently checked together
Don't use bitfields when:
- You have few flags (< 5)
- Flags have complex relationships
- You need human-readable configuration
- Flags change frequently during runtime
Important Notes
Always use bigint
type to declare bitfields and serialize it to a string to store it or transfer it over the wire because JavaScript numbers are limited to 32-bit bitwise operations.
// ❌ Wrong - limited to 32-bit operations
const permissions: number = 1 << 32; // 1 (wraps around)
// ✅ Correct - use bigint for unlimited precision
const permissions: bigint = 1n << 32n; // 4294967296n (exact)
Conclusion
Bitfields are a powerful tool for managing permissions and feature flags efficiently. They provide excellent performance and memory usage at the cost of some readability. Use them when you need to manage many boolean flags and performance is important.
The key is to use bigint
for large bitfields, serialize to strings for storage, and use bit shift operations (1 << n
) instead of magic numbers for better maintainability.