Build a Feature Flag System
Introduction
Feature flags (or feature toggles) are a powerful technique that allows you to enable or disable features without deploying new code. They enable continuous deployment, A/B testing, and gradual rollouts of new features.
In this tutorial, we'll build a complete feature flag system with targeting rules, percentage rollouts, and evaluation strategies.
What You'll Build
- A feature flag store
- Boolean and multivariate flags
- Targeting rules based on user attributes
- Percentage-based rollouts
- Flag evaluation engine
What You'll Learn
- How feature flags work
- Targeting and segmentation
- Gradual feature rollouts
- Configuration-driven feature management
Core Concepts
Flag Types
Feature flags come in several forms:
- Boolean flags - Simple on/off toggles
- Multivariate flags - Multiple variants (e.g., A/B testing)
- Percentage rollouts - Gradual enablement by percentage
Targeting
Targeting allows you to enable features for specific users based on attributes like email, region, or custom properties.
Project Overview
Our feature flag system will include:
| Feature | Description |
|---|---|
| Boolean Flags | Simple on/off toggles |
| Targeting | User attribute-based rules |
| Rollouts | Percentage-based enablement |
| Caching | In-memory flag caching |
Prerequisites
- Python 3.8+ - Installed on your system
- Basic Python knowledge - Classes, decorators
Flag Store
Create featureflags/store.py:
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field
from enum import Enum
import json
import time
class FlagType(Enum):
BOOLEAN = "boolean"
MULTIVARIATE = "multivariate"
PERCENTAGE = "percentage"
@dataclass
class TargetingRule:
attribute: str
operator: str
values: List[Any]
@dataclass
class Flag:
name: str
flag_type: FlagType
enabled: bool = False
default_value: Any = False
targeting_rules: List[TargetingRule] = field(default_factory=list)
rollout_percentage: int = 0
variants: Dict[str, Any] = field(default_factory=dict)
class FlagStore:
def __init__(self):
self._flags: Dict[str, Flag] = {}
self._cache: Dict[str, Any] = {}
self._cache_ttl = 60
def create_flag(self, name: str, flag_type: FlagType = FlagType.BOOLEAN,
default_value: Any = False) -> Flag:
flag = Flag(name=name, flag_type=flag_type, default_value=default_value)
self._flags[name] = flag
self._invalidate_cache(name)
return flag
def get_flag(self, name: str) -> Optional[Flag]:
return self._flags.get(name)
def update_flag(self, flag: Flag):
self._flags[flag.name] = flag
self._invalidate_cache(flag.name)
def delete_flag(self, name: str):
self._flags.pop(name, None)
self._invalidate_cache(name)
def get_all_flags(self) -> List[Flag]:
return list(self._flags.values())
def _invalidate_cache(self, name: str):
self._cache.pop(name, None)
def load_from_file(self, filepath: str):
with open(filepath, 'r') as f:
data = json.load(f)
for flag_data in data.get('flags', []):
rules = []
for rule_data in flag_data.get('targeting', []):
rules.append(TargetingRule(
attribute=rule_data['attribute'],
operator=rule_data['operator'],
values=rule_data['values']
))
flag = Flag(
name=flag_data['name'],
flag_type=FlagType(flag_data.get('type', 'boolean')),
enabled=flag_data.get('enabled', False),
default_value=flag_data.get('default', False),
targeting_rules=rules,
rollout_percentage=flag_data.get('rollout', 0),
variants=flag_data.get('variants', {})
)
self._flags[flag.name] = flag
def save_to_file(self, filepath: str):
flags_data = []
for flag in self._flags.values():
flag_dict = {
'name': flag.name,
'type': flag.flag_type.value,
'enabled': flag.enabled,
'default': flag.default_value,
'rollout': flag.rollout_percentage,
'variants': flag.variants,
'targeting': [
{
'attribute': rule.attribute,
'operator': rule.operator,
'values': rule.values
}
for rule in flag.targeting_rules
]
}
flags_data.append(flag_dict)
with open(filepath, 'w') as f:
json.dump({'flags': flags_data}, f, indent=2)
Flag Evaluation
Create featureflags/evaluator.py:
import hashlib
from typing import Dict, Any, Optional
from .store import FlagStore, Flag, FlagType
class EvaluationContext:
def __init__(self, user_id: str = None, attributes: Dict[str, Any] = None):
self.user_id = user_id
self.attributes = attributes or {}
class FlagEvaluator:
def __init__(self, store: FlagStore):
self.store = store
def evaluate(self, flag_name: str, context: EvaluationContext,
default: Any = None) -> Any:
flag = self.store.get_flag(flag_name)
if not flag:
return default
if not flag.enabled:
return flag.default_value
if flag.targeting_rules:
if self._evaluate_targeting(flag, context):
return self._evaluate_flag(flag, context)
if flag.rollout_percentage > 0 and context.user_id:
if self._evaluate_rollout(flag, context):
return self._evaluate_flag(flag, context)
if flag.rollout_percentage == 100:
return self._evaluate_flag(flag, context)
return flag.default_value
def _evaluate_targeting(self, flag: Flag, context: EvaluationContext) -> bool:
for rule in flag.targeting_rules:
value = context.attributes.get(rule.attribute)
if rule.operator == 'eq':
if value == rule.values[0]:
return True
elif rule.operator == 'neq':
if value != rule.values[0]:
return True
elif rule.operator == 'in':
if value in rule.values:
return True
elif rule.operator == 'contains':
if value and any(v in str(value) for v in rule.values):
return True
return False
def _evaluate_rollout(self, flag: Flag, context: EvaluationContext) -> bool:
hash_value = self._hash_for_user(flag.name, context.user_id)
bucket = hash_value % 100
return bucket < flag.rollout_percentage
def _evaluate_flag(self, flag: Flag, context: EvaluationContext) -> Any:
if flag.flag_type == FlagType.BOOLEAN:
return True
elif flag.flag_type == FlagType.MULTIVARIATE:
return self._select_variant(flag, context)
return flag.default_value
def _select_variant(self, flag: Flag, context: EvaluationContext) -> str:
hash_value = self._hash_for_user(flag.name + '_variant', context.user_id)
bucket = hash_value % 100
cumulative = 0
for variant, percentage in flag.variants.items():
cumulative += percentage
if bucket < cumulative:
return variant
return list(flag.variants.keys())[0] if flag.variants else flag.default_value
def _hash_for_user(self, flag_name: str, user_id: str) -> int:
key = f"{flag_name}:{user_id}"
hash_bytes = hashlib.md5(key.encode()).digest()
return int.from_bytes(hash_bytes[:2], 'big')
def is_enabled(self, flag_name: str, context: EvaluationContext = None) -> bool:
context = context or EvaluationContext()
result = self.evaluate(flag_name, context, False)
return bool(result)
Targeting Rules
Add advanced targeting capabilities:
# Create flags with targeting
store = FlagStore()
# Boolean flag with targeting
dark_mode = store.create_flag('dark-mode', FlagType.BOOLEAN)
dark_mode.enabled = True
dark_mode.targeting_rules = [
TargetingRule(attribute='region', operator='in', values=['US', 'EU']),
TargetingRule(attribute='tier', operator='eq', values=['premium']),
]
store.update_flag(dark_mode)
# Percentage rollout
new_checkout = store.create_flag('new-checkout', FlagType.BOOLEAN)
new_checkout.enabled = True
new_checkout.rollout_percentage = 10 # 10% of users
store.update_flag(new_checkout)
# Multivariate flag for A/B testing
button_color = store.create_flag('button-color', FlagType.MULTIVARIATE)
button_color.enabled = True
button_color.variants = {'blue': 50, 'green': 30, 'red': 20}
button_color.rollout_percentage = 100
store.update_flag(button_color)
# Save configuration
store.save_to_file('flags.json')
Testing the System
from featureflags import FlagStore, FlagEvaluator, EvaluationContext, FlagType
store = FlagStore()
store.load_from_file('flags.json')
evaluator = FlagEvaluator(store)
# Test basic evaluation
context = EvaluationContext(user_id='user123', attributes={'tier': 'premium'})
result = evaluator.evaluate('dark-mode', context, False)
print(f"Dark mode enabled: {result}")
# Test percentage rollout
context1 = EvaluationContext(user_id='user001')
context2 = EvaluationContext(user_id='user999')
result1 = evaluator.is_enabled('new-checkout', context1)
result2 = evaluator.is_enabled('new-checkout', context2)
print(f"User 001: {result1}, User 999: {result2}")
# Test multivariate
variant = evaluator.evaluate('button-color', context1, 'blue')
print(f"Button variant: {variant}")
Summary
Congratulations! You've built a complete feature flag system. Here's what you learned:
- Flag Store - How to store and manage feature flags
- Flag Evaluation - How to evaluate flags with context
- Targeting Rules - How to create user-based targeting
- Rollouts - How to implement percentage-based rollouts
Possible Extensions
- Add real-time flag updates via webhooks
- Implement flag analytics and metrics
- Add flag expiration dates
- Implement flag dependencies