2025-12-21
Feature Flags at Scale: Implementation Pattern'leri ve Platform Karşılaştırması
Distributed sistemlerde feature flag implementasyonu için production odaklı bir rehber. LaunchDarkly, Unleash ve AWS AppConfig karşılaştırması ile gradual rollout, A/B testing ve technical debt yönetimi için çalışan örnekler.
Özet
Feature flag’ler, kodu production’a deploy ederken feature görünürlüğünü runtime’da kontrol etmeni sağlar. Bu rehber, scale’de feature flag implementation pattern’lerini inceliyor ve LaunchDarkly, Unleash, AWS AppConfig platformlarını karşılaştırıyor. SDK entegrasyonu, targeting rule’ları, A/B testing entegrasyonu, circuit breaker pattern’leri ve flag technical debt yönetimi gibi kritik konuları ele alacağım. TypeScript örnekleri gradual rollout, kill switch ve lifecycle management stratejilerini gösteriyor.
Deployment Koordinasyon Problemi
Distributed sistemlerle çalışırken sürekli karşılaşılan bir sorun var: feature release’leri koordine etmek bottleneck yaratıyor. Birden fazla team aynı anda feature deploy etmek istediğinde, tipik yaklaşım dikkatli planlama, gece yarısı deployment’ları ve “umarım bir şey bozulmaz” şeklinde oluyor.
Geleneksel çözüm; uzun süre yaşayan feature branch’leri; kendi sorunlarını getiriyor. Branch’ler haftalarca main’den ayrılıyor, merge conflict’ler çoğalıyor ve entegrasyon giderek acı verici hale geliyor. Feature merge edildiğinde, artık production’ı andırmayan bir koda karşı test edilmiş oluyor.
Feature flag’ler farklı bir yaklaşım sunuyor: kodu sürekli production’a deploy et, feature görünürlüğünü runtime configuration ile kontrol et. Deployment koordine etmek yerine flag rollout’ları koordine ediyorsun. Hepsi ya hiç release’ler yerine, feature’ları belirli user segment’leri için yavaşça açıyorsun.
Şuna doğru ilerleyeceğiz: tamamlanmamış feature’ları production’a disabled flag’lerin arkasında deploy etmek, production’da gerçek data ile test etmek, kullanıcıların %1’inden %100’üne kademeli açmak ve problem çıktığında anında rollback yapabilmek.
Feature Flag Type’larını Anlamak
Tüm feature flag’ler aynı amaca hizmet etmez. Bu type’ları anlamak, baştan net lifecycle beklentileri oluşturmana yardımcı olur.
Release Flag’ler (Geçici)
Release flag’ler yeni feature’ların kademeli açılımını kontrol eder. Bu flag’lerin net bir lifecycle’ı var: geliştirme sırasında oluşturuluyor, rollout sırasında kademeli olarak açılıyor, %100 adoption’a ulaştıktan sonra kaldırılıyor. Bu flag’leri gerekenden uzun tutmak technical debt yaratır.
interface ReleaseFlag {
key: 'new-checkout-flow';
type: 'release';
defaultValue: false;
temporary: true;
expiresAt: '2025-03-01'; // Oluştururken expiration date belirle
}
Experiment Flag’ler (Geçici)
Experiment flag’ler A/B testing ve multivariate experiment’ları destekler. Release flag’ler gibi bunlar da geçici; experiment süresi boyunca var olurlar ve kazanan varyasyon implement edildikten sonra kaldırılmalılar.
interface ExperimentFlag {
key: 'cta-button-color-experiment';
type: 'experiment';
variations: {
control: { color: 'blue' },
treatmentA: { color: 'green' },
treatmentB: { color: 'red' }
};
temporary: true;
expiresAt: '2025-02-15';
}
Ops Flag’ler (Kalıcı)
Ops flag’ler circuit breaker ve kill switch olarak işlev görür. Bunlar uzun ömürlü flag’ler; operational kontrol sağlarlar, yani incident sırasında veya yüksek yük durumlarında yeni kod deploy etmeden feature’ları devre dışı bırakma yeteneği.
interface OpsFlag {
key: 'enable-recommendation-engine';
type: 'ops';
defaultValue: true;
permanent: true;
purpose: 'Disable recommendation engine during high load';
}
Permission Flag’ler (Kalıcı)
Permission flag’ler user attribute’larına, subscription tier’larına veya entitlement’lara göre feature erişimini kontrol eder. SaaS uygulamalarında bu flag’ler hangi feature’ların farklı müşteri segment’lerine sunulduğunu yönetir.
interface PermissionFlag {
key: 'premium-analytics';
type: 'permission';
defaultValue: false;
permanent: true;
targetingRules: {
subscriptionTier: ['premium', 'enterprise']
};
}
Flag Lifecycle
Farklı flag type’ları farklı lifecycle pattern’lerini takip eder:
Platform Karşılaştırması: LaunchDarkly vs Unleash vs AWS AppConfig
Feature flag platform’u seçmek, maliyet, özellikler ve operational karmaşıklık arasındaki trade-off’ları değerlendirmeyi içeriyor. Her platformla çalışırken öğrendiklerimi paylaşacağım.
Özellik Karşılaştırma Matrix’i
| Özellik | LaunchDarkly | Unleash | AWS AppConfig |
|---|---|---|---|
| Hosting | Sadece SaaS | Self-hosted veya SaaS | AWS managed |
| Fiyatlandırma | Yüksek (seat + MAU) | Ücretsiz (OSS) veya ücretli SaaS | Request başına ödeme |
| SDK Olgunluğu | Mükemmel (15+ dil) | İyi (15+ SDK) | Sadece AWS SDK |
| Targeting Rule’lar | Çok gelişmiş | İyi | Temel |
| A/B Testing | Native | Entegrasyon ile | Manuel |
| Local Evaluation | Evet | Evet | Evet (extension ile) |
| Real-time Update | Evet | Evet | Polling (default 45s) |
| Audit Log’ları | Kapsamlı | Temel (ücretli tier) | CloudTrail |
LaunchDarkly: Enterprise Platform
LaunchDarkly en olgun feature flag platform’unu sağlıyor, mükemmel UI/UX ve kapsamlı özelliklerle.
Güçlü Yanları:
- Güçlü segment yetenekleriyle gelişmiş targeting rule’ları
- Built-in experimentation platform
- Streaming connection’lar ile real-time flag update’leri
- Kapsamlı audit log’ları ve change history
- Güçlü enterprise özellikleri (RBAC, SSO, compliance)
Sınırlamalar:
- Scale’de pahalı (fiyatlandırma per-seat ve per-MAU maliyetlerini birleştiriyor)
- Sadece SaaS deployment (self-hosted seçenek yok)
- Vendor lock-in endişeleri
Maliyet Örneği (10 developer, 1M MAU, Pro tier):
- Not: LaunchDarkly fiyatlandırma modeli değişti. Güncel fiyatlandırma için satış ekibiyle iletişime geç.
- Tarihsel referans: Fiyatlandırma per-seat ve per-MAU maliyetlerini birleştiriyordu
Unleash: Open Source Alternatif
Unleash, self-host etme veya managed SaaS kullanma seçeneğiyle open-source feature flag’ler sunuyor.
Güçlü Yanları:
- Open source (Apache 2.0 lisansı)
- Self-hosting seçeneğiyle tam kontrol
- Diller arası iyi SDK coverage
- Aktif community
- Büyük user base’leri için maliyet-etkin
Sınırlamalar:
- LaunchDarkly’ye göre daha az olgun UI
- Self-hosting operational overhead ekliyor
- LaunchDarkly’ye göre sınırlı experimentation özellikleri
- Temel targeting yetenekleri
Maliyet Örneği (self-hosted):
- Infrastructure: ~$200/ay (ECS/EC2, RDS)
- Operational overhead: ~$500/ay (engineering zamanı)
- Toplam: ~8,400/yıl)
AWS AppConfig: AWS-Native Çözüm
AWS AppConfig, AWS service’leriyle entegre feature flag’ler sağlıyor.
Güçlü Yanları:
- Native AWS entegrasyonu (Lambda, ECS, vb.)
- Request başına fiyatlandırma (maliyet-etkin)
- FedRAMP certified
- External service dependency yok
- Built-in validation ve rollback
Sınırlamalar:
- Temel targeting yetenekleri
- Polling-based update’ler (real-time değil)
- AWS SDK ile sınırlı
- Native A/B testing desteği yok
- Alternatiflerine göre temel dashboard
Maliyet Örneği (1M request/ay):
- Request’ler: 1M × 0.20/ay
- Configuration’lar: 10 × 5/ay
- Toplam: ~62/yıl)
Platform Seçim Framework’ü
LaunchDarkly SDK Entegrasyonu
LaunchDarkly, local evaluation yetenekleriyle olgun SDK’lar sağlıyor. İşte production-ready bir entegrasyon pattern’i.
SDK Initialization
import { init, LDClient, LDFlagSet } from '@launchdarkly/node-server-sdk';
// Singleton client initialization
let ldClient: LDClient | null = null;
export async function initializeLaunchDarkly(): Promise<LDClient> {
if (ldClient) {
return ldClient;
}
ldClient = init(process.env.LAUNCHDARKLY_SDK_KEY!, {
// Performance optimization: network call'ları azalt
streamInitialReconnectDelay: 1000,
// Daha düşük latency için local evaluation
sendEvents: true,
// Timeout configuration
timeout: 5,
});
await ldClient.waitForInitialization({ timeout: 5 });
console.log('LaunchDarkly initialized');
return ldClient;
}
Type-Safe Flag Evaluation
// Targeting için user context
interface UserContext {
key: string;
email?: string;
country?: string;
customAttributes?: Record<string, any>;
}
// Type-safe flag evaluation
export async function evaluateFlag<T>(
flagKey: string,
user: UserContext,
defaultValue: T
): Promise<T> {
const client = await initializeLaunchDarkly();
const ldUser = {
key: user.key,
email: user.email,
country: user.country,
custom: user.customAttributes,
};
return client.variation(flagKey, ldUser, defaultValue);
}
Express Middleware Entegrasyonu
import { Request, Response, NextFunction } from 'express';
export async function checkFeatureFlag(flagKey: string) {
return async (req: Request, res: Response, next: NextFunction) => {
const user = {
key: req.user?.id || 'anonymous',
email: req.user?.email,
country: req.headers['cloudfront-viewer-country'] as string,
};
const isEnabled = await evaluateFlag(flagKey, user, false);
if (!isEnabled) {
return res.status(403).json({
error: 'Feature not available'
});
}
next();
};
}
// Route'ta kullanım
app.post('/api/checkout',
checkFeatureFlag('new-checkout-flow'),
async (req, res) => {
// Yeni checkout implementation
}
);
Performance Notu: Local evaluation, latency’yi 100-500ms’den (remote call’lar) 1-5ms’ye (local cache lookup’lar) düşürüyor.
Unleash SDK Entegrasyonu
Unleash, iyi performance özellikleriyle open-source SDK’lar sağlıyor.
SDK Setup
import { initialize, isEnabled, getVariant } from 'unleash-client';
const unleash = initialize({
url: process.env.UNLEASH_URL!,
appName: 'order-service',
instanceId: process.env.HOSTNAME || 'local',
customHeaders: {
Authorization: process.env.UNLEASH_API_TOKEN!,
},
// Performance: local caching
refreshInterval: 15000, // 15 saniye
metricsInterval: 60000, // 1 dakika
});
// SDK'nın hazır olmasını bekle
unleash.on('ready', () => {
console.log('Unleash client ready');
});
unleash.on('error', (err) => {
console.error('Unleash error:', err);
});
Context-Based Evaluation
interface UnleashContext {
userId?: string;
sessionId?: string;
remoteAddress?: string;
properties?: Record<string, string>;
}
export function checkFlag(
flagName: string,
context: UnleashContext,
defaultValue = false
): boolean {
return isEnabled(flagName, context, defaultValue);
}
export function getFlagVariant(
flagName: string,
context: UnleashContext
): { name: string; payload?: any } {
return getVariant(flagName, context);
}
Kademeli Rollout Örneği
export async function processOrder(orderId: string, userId: string) {
const context = {
userId,
sessionId: orderId,
properties: {
userTier: await getUserTier(userId),
region: await getUserRegion(userId),
},
};
// Basit on/off flag
const useNewPaymentGateway = checkFlag(
'new-payment-gateway',
context,
false
);
// A/B testing için multivariate flag
const checkoutVariant = getFlagVariant('checkout-layout', context);
if (useNewPaymentGateway) {
return processWithNewGateway(orderId, checkoutVariant);
} else {
return processWithLegacyGateway(orderId);
}
}
AWS AppConfig ile Lambda Extension
AWS AppConfig, AppConfig Lambda Extension kullanarak Lambda function’larıyla iyi entegre oluyor.
SDK Entegrasyonu
import { AppConfigDataClient, StartConfigurationSessionCommand, GetLatestConfigurationCommand } from '@aws-sdk/client-appconfigdata';
interface FeatureFlagConfig {
flags: Record<string, {
enabled: boolean;
attributes?: Record<string, any>;
}>;
version: string;
}
let cachedConfig: FeatureFlagConfig | null = null;
let configToken: string | null = null;
export async function initializeAppConfig() {
const client = new AppConfigDataClient({ region: process.env.AWS_REGION });
const sessionCommand = new StartConfigurationSessionCommand({
ApplicationIdentifier: process.env.APPCONFIG_APPLICATION!,
EnvironmentIdentifier: process.env.APPCONFIG_ENVIRONMENT!,
ConfigurationProfileIdentifier: process.env.APPCONFIG_PROFILE!,
});
const response = await client.send(sessionCommand);
configToken = response.InitialConfigurationToken!;
}
export async function fetchFeatureFlags(): Promise<FeatureFlagConfig> {
if (!configToken) {
await initializeAppConfig();
}
const client = new AppConfigDataClient({ region: process.env.AWS_REGION });
const command = new GetLatestConfigurationCommand({
ConfigurationToken: configToken!,
});
const response = await client.send(command);
configToken = response.NextPollConfigurationToken!;
if (response.Configuration) {
const configString = new TextDecoder().decode(response.Configuration);
cachedConfig = JSON.parse(configString);
}
return cachedConfig!;
}
Flag’lerle Lambda Handler
export const handler = async (event: any) => {
const config = await fetchFeatureFlags();
const userId = event.requestContext.authorizer.claims.sub;
// Basit flag kontrolü
const isNewFeatureEnabled = config.flags['new-dashboard']?.enabled || false;
// Attribute-based targeting
const userTier = await getUserTier(userId);
const premiumFeaturesFlag = config.flags['premium-features'];
const hasPremiumAccess =
premiumFeaturesFlag?.enabled &&
premiumFeaturesFlag?.attributes?.allowedTiers?.includes(userTier);
if (isNewFeatureEnabled && hasPremiumAccess) {
return {
statusCode: 200,
body: JSON.stringify({ dashboard: 'new', premium: true }),
};
}
return {
statusCode: 200,
body: JSON.stringify({ dashboard: 'legacy', premium: false }),
};
};
Performance İyileştirmesi: Lambda extension kullanmak, configuration’ı local’de cache’leyerek cold start etkisini azaltıyor. Extension her 45 saniyede AppConfig’i poll ediyor ve request’leri local cache’ten serve ediyor.
Note: Function’ına AWS AppConfig Lambda Extension layer’ını ekle:
Layer ARN: arn:aws:lambda:us-east-1:027255383542:layer:AWS-AppConfig-Extension:279Extension bir sidecar process olarak çalışır ve configuration fetching/caching’i otomatik handle eder.
Targeting Rule’ları ve User Segmentation
Gelişmiş targeting, progressive rollout’ları ve user-specific feature erişimini mümkün kılıyor.
Targeting Rule Implementation
interface TargetingRule {
attribute: string;
operator: 'equals' | 'contains' | 'greaterThan' | 'lessThan' | 'regex' | 'in';
values: any[];
}
interface Segment {
name: string;
rules: TargetingRule[];
rolloutPercentage?: number;
}
interface FeatureFlagDefinition {
key: string;
defaultValue: boolean;
segments: Segment[];
}
Flag Evaluator
class FeatureFlagEvaluator {
evaluateRule(rule: TargetingRule, context: Record<string, any>): boolean {
const attributeValue = context[rule.attribute];
if (attributeValue === undefined) {
return false;
}
switch (rule.operator) {
case 'equals':
return attributeValue === rule.values[0];
case 'in':
return rule.values.includes(attributeValue);
case 'contains':
return String(attributeValue).includes(String(rule.values[0]));
case 'greaterThan':
return Number(attributeValue) > Number(rule.values[0]);
case 'lessThan':
return Number(attributeValue) < Number(rule.values[0]);
case 'regex':
const pattern = new RegExp(rule.values[0]);
return pattern.test(String(attributeValue));
default:
return false;
}
}
evaluateSegment(segment: Segment, context: Record<string, any>): boolean {
// Segment'teki tüm rule'lar eşleşmeli (AND mantığı)
const rulesMatch = segment.rules.every(rule =>
this.evaluateRule(rule, context)
);
if (!rulesMatch) {
return false;
}
// Belirtilmişse percentage rollout uygula
if (segment.rolloutPercentage !== undefined) {
const hash = this.hashUserId(context.userId);
const bucket = hash % 100;
return bucket < segment.rolloutPercentage;
}
return true;
}
evaluateFlag(
flag: FeatureFlagDefinition,
context: Record<string, any>
): boolean {
// Segment'leri sırayla kontrol et, ilk eşleşeni döndür
for (const segment of flag.segments) {
if (this.evaluateSegment(segment, context)) {
return true;
}
}
return flag.defaultValue;
}
// Percentage rollout'lar için consistent hashing
private hashUserId(userId: string): number {
let hash = 0;
for (let i = 0; i < userId.length; i++) {
const char = userId.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 32-bit integer'a çevir
}
return Math.abs(hash);
}
}
Progressive Rollout Configuration
const evaluator = new FeatureFlagEvaluator();
const premiumFeatureFlag: FeatureFlagDefinition = {
key: 'premium-analytics',
defaultValue: false,
segments: [
{
name: 'Internal employees',
rules: [
{ attribute: 'email', operator: 'contains', values: ['@company.com'] }
],
},
{
name: 'Premium tier users',
rules: [
{ attribute: 'subscriptionTier', operator: 'in', values: ['premium', 'enterprise'] }
],
},
{
name: 'Beta users gradual rollout',
rules: [
{ attribute: 'betaOptIn', operator: 'equals', values: [true] }
],
rolloutPercentage: 20, // Beta user'ların %20'si
},
],
};
const userContext = {
userId: 'user-123',
email: '[email protected]',
subscriptionTier: 'premium',
betaOptIn: true,
};
const isEnabled = evaluator.evaluateFlag(premiumFeatureFlag, userContext);
Percentage Rollout Stratejisi: User ID üzerinde consistent hashing kullan, böylece aynı user her zaman aynı deneyimi görür. Bu, user’ların farklı request’lerde feature state’inin enabled/disabled arasında değişmesini önler.
A/B Testing Entegrasyonu
Feature flag’ler, experimentation için analytics platform’larıyla iyi çalışır.
Analytics Entegrasyonu
import { init, LDClient } from '@launchdarkly/node-server-sdk';
import * as Amplitude from '@amplitude/node';
interface ExperimentContext {
userId: string;
userProperties: Record<string, any>;
eventProperties?: Record<string, any>;
}
class FeatureFlagAnalytics {
private ldClient: LDClient;
private amplitudeClient: Amplitude.Types.NodeClient;
constructor(ldKey: string, amplitudeKey: string) {
this.ldClient = init(ldKey);
this.amplitudeClient = Amplitude.init(amplitudeKey);
}
async evaluateExperiment(
experimentKey: string,
context: ExperimentContext,
defaultVariation: string
): Promise<string> {
const ldContext = {
key: context.userId,
custom: context.userProperties,
};
// LaunchDarkly'den variation al
const variation = await this.ldClient.variation(
experimentKey,
ldContext,
defaultVariation
);
// Amplitude'da experiment exposure'ı track et
await this.amplitudeClient.logEvent({
event_type: 'Experiment Viewed',
user_id: context.userId,
event_properties: {
experiment_name: experimentKey,
variation_name: variation,
...context.eventProperties,
},
user_properties: context.userProperties,
});
return variation;
}
async trackConversion(
experimentKey: string,
context: ExperimentContext,
conversionMetric: string,
value?: number
) {
await this.amplitudeClient.logEvent({
event_type: conversionMetric,
user_id: context.userId,
event_properties: {
experiment_name: experimentKey,
value,
...context.eventProperties,
},
user_properties: context.userProperties,
});
}
}
Experiment Implementation
const analytics = new FeatureFlagAnalytics(
process.env.LAUNCHDARKLY_KEY!,
process.env.AMPLITUDE_KEY!
);
export async function renderCheckoutButton(userId: string) {
const context = {
userId,
userProperties: {
accountAge: await getAccountAge(userId),
previousPurchases: await getPurchaseCount(userId),
},
};
// Button color variation al (control, green, red)
const buttonColor = await analytics.evaluateExperiment(
'checkout-button-color',
context,
'control' // Default mavi buton
);
return {
color: buttonColor === 'control' ? 'blue' : buttonColor,
experimentKey: 'checkout-button-color',
};
}
export async function handleCheckoutClick(userId: string, experimentKey: string) {
const context = {
userId,
userProperties: {},
eventProperties: {
page: 'checkout',
},
};
await analytics.trackConversion(
experimentKey,
context,
'Checkout Button Clicked'
);
}
export async function handlePurchaseComplete(
userId: string,
experimentKey: string,
amount: number
) {
const context = {
userId,
userProperties: {},
eventProperties: {
purchaseAmount: amount,
},
};
await analytics.trackConversion(
experimentKey,
context,
'Purchase Completed',
amount
);
}
Warning: A/B testing, uygun sample size hesaplaması ve statistical significance testi gerektirir. 100 user’dan sonra kazanan ilan etme; p-value < 0.05 ve yeterli sample size için bekle. Experiment’leri başlatmadan önce gerekli sample size’ı belirlemek için Evan Miller’s A/B test calculator gibi araçları kullanmayı düşün.
Kill Switch’ler ve Circuit Breaker’lar
Operational flag’ler, yeni kod deploy etmeden incident’lara hızlı yanıt vermeyi sağlıyor.
Circuit Breaker Implementation
import { EventEmitter } from 'events';
interface CircuitBreakerConfig {
flagKey: string;
errorThreshold: number; // Açılmadan önceki hata yüzdesi
timeWindow: number; // ms cinsinden time window
checkInterval: number; // Flag state'ini ne sıklıkla kontrol et
}
enum CircuitState {
CLOSED = 'CLOSED', // Normal operasyon
OPEN = 'OPEN', // Circuit breaker tetiklendi
HALF_OPEN = 'HALF_OPEN', // Service'in toparlanıp toparlanmadığını test et
}
class FeatureFlagCircuitBreaker extends EventEmitter {
private state: CircuitState = CircuitState.CLOSED;
private errors: number[] = [];
private requests: number[] = [];
private flagEnabled: boolean = true;
constructor(
private config: CircuitBreakerConfig,
private flagClient: any
) {
super();
this.startFlagMonitoring();
}
private startFlagMonitoring() {
setInterval(async () => {
// Flag'in manuel olarak devre dışı bırakılıp bırakılmadığını kontrol et (kill switch)
this.flagEnabled = await this.flagClient.variation(
this.config.flagKey,
{ key: 'system' },
true
);
if (!this.flagEnabled && this.state !== CircuitState.OPEN) {
this.openCircuit('Manual kill switch activated');
} else if (this.flagEnabled && this.state === CircuitState.OPEN) {
this.halfOpenCircuit();
}
}, this.config.checkInterval);
}
async executeWithCircuitBreaker<T>(
operation: () => Promise<T>,
fallback: () => T
): Promise<T> {
// Circuit açıksa veya flag devre dışıysa fallback kullan
if (this.state === CircuitState.OPEN || !this.flagEnabled) {
return fallback();
}
const now = Date.now();
this.requests.push(now);
try {
const result = await operation();
// HALF_OPEN state'inde başarı circuit'i kapatır
if (this.state === CircuitState.HALF_OPEN) {
this.closeCircuit();
}
return result;
} catch (error) {
this.errors.push(now);
this.checkErrorThreshold();
throw error;
} finally {
this.cleanupOldMetrics(now);
}
}
private checkErrorThreshold() {
const now = Date.now();
const recentRequests = this.requests.filter(
t => now - t < this.config.timeWindow
);
const recentErrors = this.errors.filter(
t => now - t < this.config.timeWindow
);
if (recentRequests.length === 0) return;
const errorRate = (recentErrors.length / recentRequests.length) * 100;
if (errorRate >= this.config.errorThreshold) {
this.openCircuit(`Error rate ${errorRate.toFixed(2)}% exceeded threshold`);
}
}
private openCircuit(reason: string) {
this.state = CircuitState.OPEN;
this.emit('circuit-opened', { reason, flagKey: this.config.flagKey });
console.error(`Circuit breaker OPEN: ${reason}`);
}
private halfOpenCircuit() {
this.state = CircuitState.HALF_OPEN;
this.emit('circuit-half-open', { flagKey: this.config.flagKey });
console.log('Circuit breaker HALF_OPEN: testing recovery');
}
private closeCircuit() {
this.state = CircuitState.CLOSED;
this.errors = [];
this.emit('circuit-closed', { flagKey: this.config.flagKey });
console.log('Circuit breaker CLOSED: service recovered');
}
private cleanupOldMetrics(now: number) {
this.requests = this.requests.filter(
t => now - t < this.config.timeWindow
);
this.errors = this.errors.filter(
t => now - t < this.config.timeWindow
);
}
getState(): CircuitState {
return this.state;
}
}
Production Kullanımı
const recommendationEngineBreaker = new FeatureFlagCircuitBreaker(
{
flagKey: 'enable-recommendation-engine',
errorThreshold: 50, // %50 error rate
timeWindow: 60000, // 1 dakika
checkInterval: 5000, // Her 5 saniyede flag'i kontrol et
},
ldClient
);
// Circuit breaker event'lerini monitor et
recommendationEngineBreaker.on('circuit-opened', ({ reason }) => {
console.error('ALERT: Recommendation engine circuit breaker opened:', reason);
sendPagerDutyAlert('Recommendation engine disabled', reason);
});
export async function getRecommendations(userId: string) {
return recommendationEngineBreaker.executeWithCircuitBreaker(
// Primary operation: recommendation engine'i çağır
async () => {
const response = await fetch(`https://api.recommendations.com/users/${userId}`);
if (!response.ok) throw new Error('Recommendation API failed');
return response.json();
},
// Fallback: popüler item'ları döndür
() => {
return getPopularItems(); // Basit fallback
}
);
}
Circuit Breaker State’leri:
Flag Lifecycle Yönetimi
Terk edilmiş feature flag’lerden kaynaklanan technical debt önemli bir challenge. Aktif lifecycle yönetimi olmadan flag sayısı exponential olarak büyür.
Lifecycle Tracking
interface FlagMetadata {
key: string;
type: 'release' | 'experiment' | 'ops' | 'permission';
createdAt: Date;
createdBy: string;
expiresAt?: Date;
status: 'active' | 'inactive' | 'launched' | 'deprecated';
evaluationCount: number;
lastEvaluated?: Date;
}
class FlagLifecycleManager {
private metadata: Map<string, FlagMetadata> = new Map();
private readonly INACTIVE_THRESHOLD_DAYS = 30;
private readonly STALE_FLAG_THRESHOLD_DAYS = 90;
constructor(private flagClient: any) {
this.startLifecycleMonitoring();
}
async evaluateFlag(
flagKey: string,
context: any,
defaultValue: any
): Promise<any> {
const value = await this.flagClient.variation(flagKey, context, defaultValue);
// Metadata'yı güncelle
const metadata = this.metadata.get(flagKey);
if (metadata) {
metadata.evaluationCount++;
metadata.lastEvaluated = new Date();
}
return value;
}
registerFlag(metadata: Omit<FlagMetadata, 'evaluationCount' | 'lastEvaluated'>) {
this.metadata.set(metadata.key, {
...metadata,
evaluationCount: 0,
});
}
findStaleFlags(): FlagMetadata[] {
const now = new Date();
const staleFlags: FlagMetadata[] = [];
this.metadata.forEach(flag => {
// Kalıcı flag'leri atla (ops, permission)
if (flag.type === 'ops' || flag.type === 'permission') {
return;
}
// Expire olup olmadığını kontrol et
if (flag.expiresAt && now > flag.expiresAt) {
staleFlags.push(flag);
return;
}
// Inactive olup olmadığını kontrol et (30 gün boyunca evaluation yok)
if (flag.lastEvaluated) {
const daysSinceEvaluation =
(now.getTime() - flag.lastEvaluated.getTime()) / (1000 * 60 * 60 * 24);
if (daysSinceEvaluation > this.INACTIVE_THRESHOLD_DAYS) {
flag.status = 'inactive';
staleFlags.push(flag);
}
}
// Flag'in eski ve hiç evaluate edilmemiş olup olmadığını kontrol et
const flagAge =
(now.getTime() - flag.createdAt.getTime()) / (1000 * 60 * 60 * 24);
if (flagAge > this.STALE_FLAG_THRESHOLD_DAYS && flag.evaluationCount === 0) {
staleFlags.push(flag);
}
});
return staleFlags;
}
async generateCleanupReport(): Promise<string> {
const staleFlags = this.findStaleFlags();
const report: string[] = [
'# Feature Flag Cleanup Report',
`Generated: ${new Date().toISOString()}`,
'',
'## Kaldirilmaya Hazir Flagler',
'',
];
for (const flag of staleFlags) {
report.push(`### ${flag.key}`);
report.push(`- Type: ${flag.type}`);
report.push(`- Created: ${flag.createdAt.toISOString()}`);
report.push(`- Status: ${flag.status}`);
report.push(`- Evaluations: ${flag.evaluationCount}`);
report.push(`- Last evaluated: ${flag.lastEvaluated?.toISOString() || 'Never'}`);
if (flag.expiresAt) {
report.push(`- Expired: ${flag.expiresAt.toISOString()}`);
}
report.push('');
}
return report.join('\n');
}
private startLifecycleMonitoring() {
// Haftalık cleanup kontrolü
setInterval(async () => {
const report = await this.generateCleanupReport();
console.log(report);
// Production'da: Slack'e gönder, Jira ticket oluştur, vb.
}, 7 * 24 * 60 * 60 * 1000); // Haftalık
}
}
Flag Kaldırma Süreci
Cleanup Stratejisi:
- %100 rollout’a ulaşmış flag’leri belirle (launched state)
- Flag’in her zaman aynı değeri döndürdüğünü doğrula
- Flag kodunu kaldırmak için pull request oluştur
- Deploy et ve problemleri monitor et
- Platform’da flag’i archive et
- Dokümantasyonu güncelle
Trunk-Based Development Entegrasyonu
Feature flag’ler, tamamlanmamış feature’ların main branch’te olmasını sağlayarak trunk-based development’ı mümkün kılıyor.
Feature Toggle Pattern
export class FeatureToggle {
constructor(private flagClient: any) {}
async withFeature<T>(
flagKey: string,
context: any,
newImplementation: () => Promise<T>,
legacyImplementation: () => Promise<T>
): Promise<T> {
const isEnabled = await this.flagClient.variation(
flagKey,
context,
false // Default: disabled
);
if (isEnabled) {
try {
return await newImplementation();
} catch (error) {
console.error(`Feature ${flagKey} failed, falling back:`, error);
// Error durumunda otomatik fallback
return await legacyImplementation();
}
}
return await legacyImplementation();
}
}
Progressive Implementation
const toggle = new FeatureToggle(ldClient);
export async function processPayment(orderId: string, userId: string) {
const context = { key: userId };
return toggle.withFeature(
'new-payment-processor',
context,
// Yeni implementation (development aşamasında)
async () => {
// Tamamlanmamış feature main'e merge edilebilir
// çünkü flag'in arkasında (default olarak disabled)
return newPaymentProcessor.process(orderId);
},
// Legacy implementation (production)
async () => {
return legacyPaymentProcessor.process(orderId);
}
);
}
Faydaları:
- Uzun ömürlü feature branch’leri yok
- Main branch ile sürekli entegrasyon
- Daha küçük, daha sık merge’ler
- Azaltılmış merge conflict’leri
- Daha hızlı feedback döngüleri
Testing Stratejileri
Feature-flagged kodu test etmek, hem enabled hem disabled state’lerini test etmeyi gerektirir.
Mock Flag Client
class MockFlagClient {
private flags: Map<string, any> = new Map();
setFlag(key: string, value: any) {
this.flags.set(key, value);
}
async variation(key: string, context: any, defaultValue: any): Promise<any> {
return this.flags.get(key) ?? defaultValue;
}
reset() {
this.flags.clear();
}
}
Her İki State’i Test Et
describe('Payment Processing', () => {
let mockFlags: MockFlagClient;
let paymentService: PaymentService;
beforeEach(() => {
mockFlags = new MockFlagClient();
paymentService = new PaymentService(mockFlags);
});
describe('yeni payment processor ENABLED', () => {
beforeEach(() => {
mockFlags.setFlag('new-payment-processor', true);
});
it('yeni payment processor kullanmali', async () => {
const result = await paymentService.processPayment('order-123', 'user-456');
expect(result.processor).toBe('new');
});
it('yeni processor errorlari gracefully handle etmeli', async () => {
mockNewProcessor.process = jest.fn().mockRejectedValue(new Error('API Error'));
// Legacy'e fallback etmeli
const result = await paymentService.processPayment('order-123', 'user-456');
expect(result.processor).toBe('legacy');
});
});
describe('yeni payment processor DISABLED', () => {
beforeEach(() => {
mockFlags.setFlag('new-payment-processor', false);
});
it('legacy payment processor kullanmali', async () => {
const result = await paymentService.processPayment('order-123', 'user-456');
expect(result.processor).toBe('legacy');
});
it('yeni processor cagirmamali', async () => {
const newProcessorSpy = jest.spyOn(mockNewProcessor, 'process');
await paymentService.processPayment('order-123', 'user-456');
expect(newProcessorSpy).not.toHaveBeenCalled();
});
});
});
Warning: Her feature flag kombinasyonunu test etme. 10 flag ile bu 1,024 test case demek. Bunun yerine:
- Kritik feature’ları hem ON hem OFF ile test et
- Risk-based testing kullan (riskli feature’ları daha kapsamlı test et)
- Öngörülebilir davranış için mock flag client kullan
- Integration testleri dedicated test environment flag’leri kullanır
Ana Çıkarımlar
Net Flag Type’larıyla Başla: Baştan release (geçici), experiment (geçici), ops (kalıcı) ve permission (kalıcı) flag’leri ayırt et. Bu, net lifecycle beklentileri oluşturur.
Platform’u İhtiyaçlara Göre Seç: AWS-native uygulamalar AppConfig’in maliyet yapısından ve entegrasyonundan faydalanır. Kompleks targeting gereksinimleri LaunchDarkly’nin gelişmiş yeteneklerini tercih eder. Teknik uzmanlığa sahip budget-conscious team’ler self-hosted Unleash’i düşünmeli.
Performance Local Evaluation Gerektirir: SDK caching ve local evaluation, latency’yi 100-500ms’den (remote call’lar) 1-5ms’ye (local lookup’lar) düşürür. Bu, yüksek trafikli uygulamalar için önemli.
Flag Debt Hızla Birikir: Lifecycle yönetimi olmadan flag sayısı exponential olarak büyür. Flag oluştururken expiration date belirle, stale flag detection’ı otomatikleştir ve düzenli cleanup planla.
Güvenli Değerlere Default Et: Feature flag’ler stable/legacy davranışa default olmalı. Bu, flag service’i kullanılamaz hale gelirse graceful degradation sağlar.
Her İki Code Path’i Test Et: Her flag iki execution path oluşturur. Production’a çıkmadan önce sorunları yakalamak için her ikisini de CI/CD pipeline’ında test et.
Kademeli Rollout Monitoring Gerektirir: Progressive rollout’lar (%1 → %5 → %25 → %50 → %100) kapsamlı monitoring ve net rollback kriterleri gerektirir. Rollout’ları başlatmadan önce error rate threshold’larını belirle.
Kill Switch’ler MTTR’yi Azaltır: Otomatik feature devre dışı bırakma ile circuit breaker’lar, incident sırasında mean time to recovery’yi saatlerden dakikalara düşürebilir.
Trunk-Based Development Çalışır: Feature flag’ler, uzun ömürlü feature branch’leri olmadan sürekli entegrasyon sağlar. Tamamlanmamış feature’ları disabled flag’lerin arkasında production’a deploy et.
A/B Testing İstatistiksel Titizlik Gerektirir: Uygun experiment tasarımı sample size hesaplaması, statistical significance testi (p < 0.05) ve novelty effect gibi yaygın bias’lardan kaçınmayı içerir.
Feature flag’ler, team’lerin yazılımı nasıl deploy ettiğini dönüştürüyor; koordineli big-bang release’lerden sürekli, kontrollü rollout’lara. Anahtar, flag’leri mimarinde first-class citizen olarak ele almak ve uygun lifecycle yönetimiyle technical debt’e dönüşmesini önlemek.
İlgili yazılar
TypeScript microservislerde consumer-driven contract testing'i Pact ile uygulamaya yönelik pratik bir kılavuz. Breaking API değişikliklerini deployment öncesi yakalayın ve integration test yükünü azaltın.
AWS Lambda, API Gateway, DynamoDB ve Step Functions için hızlı geri bildirim ve production güvenilirliği sağlayan kapsamlı bir test stratejisi oluşturmayı öğrenin.
AWS Bedrock AgentCore'un agentic AI'ı ölçekte deploy etme altyapı zorluklarını nasıl çözdüğünü öğrenin - prototipten production'a runtime, memory, gateway ve multi-agent koordinasyonu ile.
Nested stack'ler, cross-stack referanslar, SSM Parameter Store ve microstack mimarisi kullanarak CloudFormation'un 500 kaynak sınırını aşmak için kanıtlanmış stratejiler. TypeScript CDK örnekleri ve karar çerçeveleri ile.
AgentCore Runtime üzerinde minimal bir Strands agent'ı CDK ile deploy etme rehberi — parametrize stack, arm64 build, deploy ve invoke akışı, ve ilk çağrıdan önce gereken IAM ve Marketplace ön koşulları.