LaunchDarkly vs Unleash vs AWS AppConfig: Feature Flag Karşılaştırması
Distributed sistemlerde feature flag için production rehberi: LaunchDarkly, Unleash ve AWS AppConfig karşılaştırması, rollout ve A/B testing örnekleri.
Feature flag’ler, kodu production’a deploy ederken feature görünürlüğünü runtime’da kontrol etmeni sağlar. Bu kontrol olmadan, distributed bir sistemde feature deploy etmek iki kötü seçeneğe sıkıştırır: kesinti riski taşıyan big-bang release’ler ya da merge conflict biriktiren uzun ömürlü branch’ler. Her deployment, iş takvimine bağlanmış bir ya hep ya hiç olayına dönüşür.
Sahayı büyük ölçüde üç platform kaplıyor ve makul varsayılan, sistemin zaten nerede çalıştığına bağlı. Halihazırda AWS üzerinde olan ve targeting ihtiyacı “hangi environment, hangi tier, yüzde kaç” seviyesinde kalan bir ekip için en ucuz giriş AWS AppConfig. Request başına fiyatlanıyor, request yoluna ek bir vendor koymuyor, validation ve rollback’i deployment’ın içine gömüyor. Segment mantığı ve native experimentation zaten flag istemenin asıl sebebiyse LaunchDarkly fiyatını hak ediyor. Flag verisinin kendi altyapısından çıkmaması gereken ve servisi işletmeyi göze alan ekipler içinse self-hosted Unleash doğru takas.
Deployment Koordinasyonu Sorunu#
Geleneksel çözüm, yani 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.
Flag’ler devredeyken deployment değil rollout koordine ediyorsun. Release, tek seferde tek bir user segment’ine uygulanan ve aynı şekilde geri alınan bir configuration değişikliğine dönüşüyor.
Hedef durum net: tamamlanmamış feature’lar production’da disabled flag’lerin arkasında duruyor, gerçek data ile test ediliyor, kullanıcıların %1’inden %100’üne kademeli açılıyor ve bir şey bozulduğunda anında kapatılıyor.
Dört Flag Türü, İki Farklı Ömür#
Release Flag’ler#
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#
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 ve Permission Flag’leri#
Kalan iki tür kalıcı. Ops flag’ler circuit breaker ve kill switch olarak işlev görür: incident sırasında ya da yüksek yük altında, yeni kod deploy etmeden feature’ı devre dışı bırakabilmek.
interface OpsFlag {
key: 'enable-recommendation-engine';
type: 'ops';
defaultValue: true;
permanent: true;
purpose: 'Disable recommendation engine during high load';
}
Permission flag’ler başka bir soruyu yanıtlıyor: feature’ı hangi müşteriler alacak. User attribute’larına, subscription tier’larına veya entitlement’lara göre çalışırlar; SaaS uygulamalarında hangi feature’ın hangi müşteri segmentine açık olduğunu bunlar belirler.
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:
Üçü Nerede Ayrışıyor#
Feature flag platform’u seçmek, maliyeti yetenek ve operasyonel yük ile takas etmek demek. Ayrışma üç noktada beliriyor: evaluation’ın nerede yapıldığı, bir değişikliğin ne kadar hızlı yayıldığı ve servisi kimin işlettiğ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#
Targeting, LaunchDarkly’nin öne çıktığı yer: segment kuralları diğer ikisinden daha derine iniyor ve experimentation platformla birlikte geliyor. Flag update’leri streaming connection’lar üzerinden real-time yayılıyor, audit log’ları ve change history kapsamlı, enterprise kontroller (RBAC, SSO, compliance) yerinde. İş sözleşmeye gelince değişiyor. Fiyatlandırma per-seat ve per-MAU maliyetlerini birleştiriyor ve bu plan yapısı yeterince sık değiştiği için buraya yazılacak her rakam kısa sürede eskiyor; bütçelemeden önce somut bir seat sayısı ve MAU hacmini güncel plan sayfasından fiyatla. Deployment sadece SaaS olduğu için geçiş maliyetini de aynı anda hesaplamakta fayda var.
Unleash#
Unleash, üçü içinde kendi altyapında çalıştırabileceğin tek seçenek. Proje Apache 2.0 lisanslı, diller arası SDK coverage iyi, community aktif; buna karşılık UI LaunchDarkly’ye göre daha az olgun, targeting temel seviyede kalıyor ve experimentation özellikleri daha sınırlı. Asıl satın aldığın şey self-hosting: flag verisi kendi altyapından çıkmıyor, karşılığında bir servis ve bir veritabanı işletiyorsun; ECS/EC2 ve RDS tarafında ayda ~$200 mertebesinde bir maliyet, artı bunları ayakta tutacak engineering zamanı.
AWS AppConfig#
AWS AppConfig’te targeting temel seviyede kalıyor, update’ler push yerine polling üzerinden geliyor, SDK yüzeyi AWS ile sınırlı: native A/B testing desteği yok ve dashboard alternatiflerine göre yetersiz. Karşılığında Lambda, ECS ve AWS compute yüzeyinin tamamıyla native entegrasyon geliyor; FedRAMP certified, request yoluna eklenecek harici bir service dependency yok, request başına fiyatlandırma scale’de maliyet-etkin kalmasını sağlıyor ve validation ile rollback deployment akışının içine gömülü. Ayda 1M request için:
- Request’ler: 1M × $0.0000002 = $0.20/ay
- Configuration’lar: 10 × $0.50 = $5/ay
- Toplam:
$5.20/ay ($62/yıl)
Hangisini Seçmeli#
LaunchDarkly SDK Entegrasyonu#
Client’ı bir kere, singleton olarak initialize et ve request lifecycle boyunca aynı instance’ı kullan:
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
}
);
Local evaluation’da SDK cevabı bellekteki rule set’ten veriyor; flag kontrolü network round trip yerine bir map lookup’a mal oluyor. Bu overhead, bir request yolunda birkaç flag değerlendirilse bile düşük kalıyor.
Unleash SDK Entegrasyonu#
Burada Unleash polling’i 15 saniyelik aralıkla çalışıyor; başlangıç yönetimi için ready/error event’leri var.
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#
Her poll sadece değişeni getiriyor, çünkü client request’ler arasında bir configuration token takip ediyor.
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’li 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 }),
};
};
Lambda extension configuration’ı local’de cache’liyor; cold start etkisini düşüren şey bu. Extension her 45 saniyede AppConfig’i poll ediyor ve request’leri o cache’ten karşılıyor.
Note
Function’ına AWS AppConfig Lambda Extension layer’ını ekle:
Layer ARN: arn:aws:lambda:us-east-1:027255383542:layer:AWS-AppConfig-Extension:279
Extension bir sidecar process olarak çalışır ve configuration fetching/caching’i otomatik handle eder.
Targeting Rule’ları ve User Segmentation#
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);
User ID üzerinde consistent hashing, aynı user’ı hep bölünmenin aynı tarafında tutar. Olmadığında feature state, request’ten request’e enabled ile disabled arasında gidip gelir.
A/B Testing Entegrasyonu#
Flag platformu varyasyonu seçiyor, analytics platformu exposure’ı kaydediyor; iki tarafın da aynı user key’i görmesi gerekiyor.
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’i başlatmadan önce gerekli sample size’ı belirlemek için Evan Miller’ın A/B test calculator’ı gibi araçlara bak.
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
}
);
}
Flag Lifecycle Yönetimi#
Kimsenin kaldırmadığı flag’ler technical debt’e dönüşüyor. Aktif lifecycle yönetimi olmadan flag sayısı büyümeye devam ediyor.
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#
Launched bir flag’i kaldırmak sabit bir sırayla ilerliyor:
- %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);
}
);
}
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 İçin Testler#
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
Varsayılanı Ne Zaman Değiştirmeli#
Flag’ler operasyonel kaldığı sürece varsayılan AppConfig olarak duruyor: kill switch’ler, ops toggle’ları ve AWS compute içinde değerlendirilen release flag’leri. Bu iş için 45 saniyelik polling ve attribute seviyesinde targeting yeterli.
Bu varsayılanı iki durum bozar. Segment mantığı her hafta değişiyorsa ya da flag’ler zaten deneyleri istatistiksel olarak okuyabilmek için varsa, LaunchDarkly’nin targeting ve experimentation tarafı seat maliyetini karşılıyor. Flag verisinin kendi altyapından çıkmaması gerekiyorsa self-hosted Unleash doğru takas: kontrol karşılığında işletilecek bir servis ve bir veritabanı. Hangisinde karar kılarsan kıl, release ve experiment flag’lerine oluşturma anında bir son kullanma tarihi ver ve her flag’in default’u stabil yol olsun; böylece flag servisine ulaşılamadığında sistem eski davranışla çalışmaya devam eder.
Kaynaklar#
- Feature Toggle’lar (Feature Flag’ler) - Martin Fowler (yeni sekmede açılır) - Release, deney, operasyon ve izin toggle’ları için kapsamlı kalıplar
- OpenFeature: Giriş (yeni sekmede açılır) - Satıcıdan bağımsız feature flag API’leri için CNCF standardı
- LaunchDarkly: Feature Flag En İyi Uygulamaları (yeni sekmede açılır) - Flag yaşam döngüsü yönetimi için üretim rehberleri
- OpenFeature: Beş Dakikada Feature Flag (yeni sekmede açılır) - Standartlaştırılmış feature flagging için hızlı başlangıç
- Martin Fowler: Feature Flag bliki (yeni sekmede açılır) - Feature flag’lerin özlü tanımı ve sınıflandırması
İlgili yazılar
TypeScript microservislerde Pact ile consumer-driven contract testing: breaking API değişikliklerini deployment öncesi yakalayın, integration test yükünü azaltın.
testing · microservices · api-design +3
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.
lambda · testing · serverless +8
Git branching stratejilerinin takım büyüklüğü, ürün tipi ve release temposuyla eşleşmesi. Varsayılan GitHub Flow; başka bir model yükünü ne zaman hak eder?
ci-cd · lessons-learned · team-management +3
Yüksek performanslı ekiplerin kod-tamam ile canlı arasındaki süreyi güvenlikten ve kaliteden ödün vermeden nasıl kısalttığı. Teknik liderler için bir karar rehberi.
ci-cd · devops · dora-metrics +2
Üretim deploy'ları gerçek bir onay adımı ister: GitHub Environment, native koruma kuralları ve environment'a bağlı secret'lar; if: hilesi ya da marketplace değil.
github-actions · ci-cd · devops +2