Feature flag (feature toggle) modern software development’in temel araçlarından. Deploy ile feature release’i ayırıyor. Production’da kodunuz var ama kullanıcıya gösterilmiyor. Switch’i aç, feature live.
A/B test, canary deploy, gradual rollout, kill switch – hepsi feature flag ile mümkün. Bu yazıda kendi flag sisteminizi 4 aşamada nasıl kurabileceğinizi anlatacağım.
Aşama 1: Simple config flags
Başlangıç: config file’da basit boolean flag’ler.
# config/features.yml
features:
new_checkout: false
dark_mode: true
experimental_search: falseCode’da kullanım:
if config.features.new_checkout:
render_new_checkout()
else:
render_old_checkout()Avantaj:
– Super simple setup
– No external dependency
– Version controlled (config Git’te)
Dezavantaj:
– Flag değiştirmek için deploy gerekiyor
– User-level targeting yok
– A/B test imkansız
– Real-time toggle yok
Bu aşama küçük proje için OK. Büyüdükçe yetersiz.
Aşama 2: Database-backed flags
Flag’leri database’de tut. Admin panel’den değiştirilebilir.
Schema:
CREATE TABLE feature_flags (
key VARCHAR(100) PRIMARY KEY,
enabled BOOLEAN DEFAULT FALSE,
description TEXT,
updated_at TIMESTAMP
);Code:
def is_enabled(flag_key: str) -> bool:
flag = db.query("SELECT enabled FROM feature_flags WHERE key = ?", flag_key)
return flag and flag.enabledAdmin panel: Basit UI, flag toggle.
Avantaj:
– Real-time toggle (deploy olmadan)
– Admin panel control
– Audit log (kim ne zaman toggle etti)
Dezavantaj:
– Her flag check database call (performans issue)
– User-level targeting hâlâ yok
– A/B test hâlâ yok
Fix: caching layer. Redis’te flag state’leri cache’le, 30 saniye TTL.
Aşama 3: User-targeted flags
Bazı user’lara show, bazılarına don’t. Beta testers, internal team, premium users.
Schema:
CREATE TABLE feature_flags (
key VARCHAR(100) PRIMARY KEY,
enabled BOOLEAN,
rollout_percentage INT, -- 0-100
user_groups JSON, -- ["beta", "internal"]
user_whitelist JSON -- specific user_ids
);Code:
def is_enabled(flag_key: str, user_id: str, user_groups: list) -> bool:
flag = get_flag(flag_key)
if not flag.enabled:
return False
# Whitelist check
if user_id in flag.user_whitelist:
return True
# Group check
if any(g in flag.user_groups for g in user_groups):
return True
# Rollout percentage (deterministic hash)
user_hash = int(md5(flag_key + user_id).hexdigest(), 16)
if (user_hash % 100) < flag.rollout_percentage:
return True
return FalseGradual rollout:
– Start: 1% rollout
– If no issues: 5%, 10%, 25%, 50%, 100%
– Issue detected: rollback instantly
Avantaj:
– User targeting
– Gradual rollout
– Kill switch
– A/B test foundations
Dezavantaj:
– Complexity yüksek
– Analytics integration lazım A/B test için
Aşama 4: Full A/B testing
A/B testing için feature flag’ler + analytics + statistical significance.
Enhanced flag:
CREATE TABLE experiments (
key VARCHAR(100) PRIMARY KEY,
variants JSON, -- [{"name": "control", "weight": 50}, {"name": "new_ui", "weight": 50}]
active BOOLEAN,
started_at TIMESTAMP
);
CREATE TABLE experiment_exposures (
experiment_key VARCHAR(100),
user_id VARCHAR(100),
variant VARCHAR(50),
exposed_at TIMESTAMP,
INDEX (experiment_key, user_id)
);Code:
def get_variant(experiment_key: str, user_id: str) -> str:
experiment = get_experiment(experiment_key)
# Existing assignment?
existing = get_exposure(experiment_key, user_id)
if existing:
return existing.variant
# New assignment (hash-based)
user_hash = int(md5(experiment_key + user_id).hexdigest(), 16) % 100
cumulative = 0
for variant in experiment.variants:
cumulative += variant.weight
if user_hash < cumulative:
record_exposure(experiment_key, user_id, variant.name)
return variant.nameAnalytics integration:
# Track experiment metric
analytics.track("purchase", {
"user_id": user_id,
"amount": 100,
"experiment_new_checkout": get_variant("new_checkout", user_id)
})Later, analyze:
– Variant A conversion rate: 5.2%
– Variant B conversion rate: 6.8%
– Statistical significance: p < 0.01
– Variant B wins, roll out fully
Third-party solutions
Kendi sistemini kurmak istemiyorsan:
LaunchDarkly: Enterprise-grade feature flag management. $$$.
Unleash: Open source, self-hosted.
Split.io: Feature flag + A/B test. Mid-market.
Flagsmith: Open source + managed options.
Firebase Remote Config: Google’ın ücretsiz seçeneği.
Trade-off: vendor dependency, pricing, customization.
Küçük projede Firebase Remote Config yeter. Orta ölçek LaunchDarkly. Enterprise self-hosted Unleash.
Flag hygiene
Feature flag’lerin yaşam döngüsü var. Ignoring → flag debt.
Types:
– Release flags: Feature deployment için. Short-lived (weeks).
– Experiment flags: A/B test için. Medium-lived (months).
– Ops flags: Kill switch, circuit breaker. Long-lived.
– Permission flags: Premium features. Permanent.
Cleanup discipline:
– Release flag successful → merge code, remove flag within 2 weeks
– Experiment concluded → make winning variant default, remove flag
– Ops flags review quarterly
Flag debt: 6 ay önce eklenen flag’ler hâlâ code’da. Her flag cognitive overhead.
Performance considerations
Flag check her request’te happening. Overhead minimize:
- In-memory cache. Flag state’leri app memory’de, periodic refresh.
- Batch fetching. User login’de tüm flag’leri fetch, session boyunca kullan.
- Edge-deployed flags. CDN level flag evaluation.
Cache TTL balance: uzun TTL → stale flag, kısa TTL → DB pressure.
Testing with flags
Flag’li kod test etme:
@pytest.fixture
def enable_flag(flag_key):
with mock.patch('features.is_enabled', return_value=True):
yield
def test_new_checkout_flow(enable_flag):
# Test with flag ON
...Every major flag için her iki path’i test et. “Flag on” ve “flag off” scenarios.
Sonuç
Feature flag’ler deploy risk’ini azaltıyor. Gradual rollout, kill switch, A/B test – hepsini enable ediyor.
4 aşamalı adoption: config → DB → user-targeted → full A/B. Her aşama önceki’yi genişletiyor. Projenizin büyüklüğüne göre başlayın.
Flag hygiene unutmayın. Flag’ler deprecated olmadığında code complexity artıyor. Cleanup discipline gerekli.