İçeriğe atla

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.

Ayhan Sipahi Ayhan Sipahi

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:

Release

Experiment

Ops

Permission

Feature Flag Olusturuldu

Flag Type?

Kademeli Rollout

A/B Test

Circuit Breaker

Access Control

Yuzde 100 Rollout

Flag Kaldir

Sonuclari Analiz Et

Kazanani Implement Et

Uzun-omurlu

Üçü 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.

ÖzellikLaunchDarklyUnleashAWS AppConfig
HostingSadece SaaSSelf-hosted veya SaaSAWS managed
FiyatlandırmaYüksek (seat + MAU)Ücretsiz (OSS) veya ücretli SaaSRequest başına ödeme
SDK OlgunluğuMükemmel (15+ dil)İyi (15+ SDK)Sadece AWS SDK
Targeting Rule’larÇok gelişmişİyiTemel
A/B TestingNativeEntegrasyon ileManuel
Local EvaluationEvetEvetEvet (extension ile)
Real-time UpdateEvetEvetPolling (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#

Evet

Hayir

Evet

Hayir

Evet

Hayir

Yuksek

Orta

Dusuk

Feature Flag Platform Sec

Zaten AWS'de misin?

Kompleks targeting lazim?

Budget kisitlamasi?

LaunchDarkly

AWS AppConfig

Teknik uzmanligi?

Unleash Self-hosted

Unleash SaaS

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
    }
  );
}

Error threshold asildi

Kill switch aktive edildi

Flag yeniden aktive edildi

Request basarili

Request basarisiz

Manuel mudahale

CLOSED

OPEN

HALF_OPEN

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#

Deploy

Evaluation yok

Yuzde 100 rollout

Kaldirilmayi planla

Code cleanup

Stale flag

Created

Active

Inactive

Launched

Deprecated

Removed

Launched bir flag’i kaldırmak sabit bir sırayla ilerliyor:

  1. %100 rollout’a ulaşmış flag’leri belirle (launched state)
  2. Flag’in her zaman aynı değeri döndürdüğünü doğrula
  3. Flag kodunu kaldırmak için pull request oluştur
  4. Deploy et ve problemleri monitor et
  5. Platform’da flag’i archive et
  6. 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#

İlgili yazılar