İçeriğe atla

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:

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

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

Ö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: 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: ~700/ay( 700/ay (~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.0000002=0.0000002 = 0.20/ay
  • Configuration’lar: 10 × 0.50=0.50 = 5/ay
  • Toplam: ~5.20/ay( 5.20/ay (~62/yıl)

Platform Seçim Framework’ü

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

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:279

Extension 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:

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

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

Deploy

Evaluation yok

Yuzde 100 rollout

Kaldirilmayi planla

Code cleanup

Stale flag

Created

Active

Inactive

Launched

Deprecated

Removed

Cleanup Stratejisi:

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

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

Pact ile Contract Testing - Microservislerde API Uyumluluğunu Sağlama

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.

testingmicroservicesapi+7
Serverless Uygulamaları Test Etmek: Pratik Bir Strateji Rehberi

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.

lambdatestingserverless+11
AWS Bedrock AgentCore ile Production-Ready AI Agentları Geliştirmek

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.

aws-bedrockai-agentsagentic-ai+4
CloudFormation'un 500 Kaynak Sınırını Aşmak: Büyük Ölçekli Altyapı için Pratik Stratejiler

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.

aws-cdkcloudformationinfrastructure-as-code+4
AWS Bedrock AgentCore'u CDK ile deploy etmek: hızlı başlangıç

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ı.

aws-bedrockai-agentsaws-cdk+3