# iOS Feature Flags Stratejisi: Güvenli ve Kontrollü Yayın
Bir özelliği App Store'a gönderdikten sonra geri almak istediğiniz oldu mu? Ya da yeni bir UI'ı sadece kullanıcılarınızın yüzde onuna göstermek? Feature flags, modern mobil geliştirmenin vazgeçilmez aracıdır. Bu rehberde, basit boolean flag'lerden karmaşık A/B test senaryolarına kadar her şeyi production-ready kodlarla öğreneceksiniz.
Uyarı: Feature flags yanlış yönetildiğinde teknik borca dönüşür. Bu rehberdeki lifecycle yönetimi bölümünü mutlaka okuyun.
İçindekiler
- Feature Flags Temelleri
- Mimari Tasarım
- Local vs Remote Flags
- A/B Testing Entegrasyonu
- Kill Switch Mekanizması
- Kademeli Yayın Stratejisi
- Flag Lifecycle Yönetimi
- Karşılaştırma Tablosu
- ALTIN İPUCU
- Sonuç ve Öneriler
1. Feature Flags Temelleri
Feature flag (ya da feature toggle), kodunuzdaki bir özelliğin çalışma zamanında açılıp kapatılmasını sağlayan mekanizmadır.
Flag Türleri
Tür | Ömür | Kullanım Amacı | Örnek |
|---|---|---|---|
**Release Flag** | Kısa (1-2 sprint) | Tamamlanmamış özelliği gizleme | Yeni profil sayfası |
**Experiment Flag** | Orta (2-4 hafta) | A/B testing | Checkout akışı varyantları |
**Ops Flag** | Uzun | Operasyonel kontrol | Maintenance modu |
**Kill Switch** | Kalıcı | Acil kapatma mekanizması | Ödeme sistemi devre dışı |
**Permission Flag** | Kalıcı | Kullanıcı bazlı erişim | Premium özellikler |
2. Mimari Tasarım
İyi bir feature flag sistemi, temiz mimari prensiplerine uygun olmalıdır.
Protocol-Based Tasarım
swift
1// MARK: - Feature Flag Protocol2protocol FeatureFlagProviding {3 func isEnabled(_ flag: FeatureFlag) -> Bool4 func value<T>(for flag: FeatureFlag, defaultValue: T) -> T5 func refresh() async throws6}7 8// MARK: - Feature Flag Tanımları9enum FeatureFlag: String, CaseIterable, Codable {10 // Release Flags11 case newProfilePage = "new_profile_page"12 case redesignedCheckout = "redesigned_checkout"13 case darkModeV2 = "dark_mode_v2"14 15 // Experiment Flags16 case onboardingVariantB = "onboarding_variant_b"17 case pricingExperiment = "pricing_experiment"18 19 // Kill Switches20 case paymentSystemEnabled = "payment_system_enabled"21 case pushNotificationsEnabled = "push_notifications_enabled"22 23 // Ops Flags24 case maintenanceMode = "maintenance_mode"25 case debugMenuEnabled = "debug_menu_enabled"26 27 var defaultValue: Bool {28 switch self {29 case .paymentSystemEnabled, .pushNotificationsEnabled:30 return true // Kill switch'ler varsayilan acik31 case .maintenanceMode:32 return false33 default:34 return false35 }36 }37 38 var flagType: FlagType {39 switch self {40 case .newProfilePage, .redesignedCheckout, .darkModeV2:41 return .release42 case .onboardingVariantB, .pricingExperiment:43 return .experiment44 case .paymentSystemEnabled, .pushNotificationsEnabled:45 return .killSwitch46 case .maintenanceMode, .debugMenuEnabled:47 return .ops48 }49 }50}51 52enum FlagType: String {53 case release, experiment, killSwitch, ops, permission54}Feature Flag Service
swift
1// MARK: - Feature Flag Service2final class FeatureFlagService: FeatureFlagProviding {3 static let shared = FeatureFlagService()4 5 private var remoteFlags: [String: Any] = [:]6 private var localOverrides: [String: Any] = [:]7 private let cache: UserDefaults8 private let lock = NSLock()9 10 private init(cache: UserDefaults = .standard) {11 self.cache = cache12 loadCachedFlags()13 }14 15 func isEnabled(_ flag: FeatureFlag) -> Bool {16 lock.lock()17 defer { lock.unlock() }18 19 // 1. Local override (debug menusunden)20 if let override = localOverrides[flag.rawValue] as? Bool {21 return override22 }23 24 // 2. Remote deger25 if let remote = remoteFlags[flag.rawValue] as? Bool {26 return remote27 }28 29 // 3. Cache30 if cache.object(forKey: "ff_\(flag.rawValue)") != nil {31 return cache.bool(forKey: "ff_\(flag.rawValue)")32 }33 34 // 4. Default35 return flag.defaultValue36 }37 38 func value<T>(for flag: FeatureFlag, defaultValue: T) -> T {39 lock.lock()40 defer { lock.unlock() }41 42 if let override = localOverrides[flag.rawValue] as? T {43 return override44 }45 if let remote = remoteFlags[flag.rawValue] as? T {46 return remote47 }48 return defaultValue49 }50 51 func refresh() async throws {52 let url = URL(string: "https://api.example.com/v1/feature-flags")!53 let (data, _) = try await URLSession.shared.data(from: url)54 let flags = try JSONDecoder().decode([String: AnyCodable].self, from: data)55 56 lock.lock()57 remoteFlags = flags.mapValues { item in item.value }58 cacheFlags()59 lock.unlock()60 61 NotificationCenter.default.post(name: .featureFlagsUpdated, object: nil)62 }63 64 // MARK: - Debug65 func setOverride(_ value: Bool, for flag: FeatureFlag) {66 lock.lock()67 localOverrides[flag.rawValue] = value68 lock.unlock()69 }70 71 func clearOverrides() {72 lock.lock()73 localOverrides.removeAll()74 lock.unlock()75 }76 77 private func loadCachedFlags() {78 for flag in FeatureFlag.allCases {79 let key = "ff_\(flag.rawValue)"80 if cache.object(forKey: key) != nil {81 remoteFlags[flag.rawValue] = cache.bool(forKey: key)82 }83 }84 }85 86 private func cacheFlags() {87 for (key, value) in remoteFlags {88 cache.set(value, forKey: "ff_\(key)")89 }90 }91}92 93extension Notification.Name {94 static let featureFlagsUpdated = Notification.Name("featureFlagsUpdated")95}Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
3. Local vs Remote Flags
Kriter | Local Flags | Remote Flags | Hybrid |
|---|---|---|---|
**Hız** | Anında | Network gecikmesi | Cached + async |
**Esneklik** | App update gerekli | Anlık değişiklik | En iyi ikisi |
**Offline** | Her zaman çalışır | Cache'e bağlı | Cache fallback |
**Maliyet** | Ücretsiz | Servis maliyeti | Servis maliyeti |
**Kontrol** | Geliştirici | Product/Ops team | Her iki taraf |
**Güvenlik** | Reverse engineer riski | Sunucu tarafı güvenli | Sunucu tarafı güvenli |
4. A/B Testing Entegrasyonu
swift
1// MARK: - Experiment Manager2final class ExperimentManager {3 static let shared = ExperimentManager()4 5 private let flagService: FeatureFlagProviding6 private let analytics: AnalyticsProviding7 private var assignments: [String: String] = [:]8 9 init(10 flagService: FeatureFlagProviding = FeatureFlagService.shared,11 analytics: AnalyticsProviding = AnalyticsService.shared12 ) {13 self.flagService = flagService14 self.analytics = analytics15 }16 17 func variant(for experiment: Experiment) -> ExperimentVariant {18 // Daha once atanmis mi kontrol et19 if let cached = assignments[experiment.rawValue],20 let variant = ExperimentVariant(rawValue: cached) {21 return variant22 }23 24 // Deterministic assignment (kullanici ID hash'i ile)25 let userId = UserSession.current.userId26 let hash = stableHash("\(experiment.rawValue)_\(userId)")27 let bucket = hash % 10028 29 let variant: ExperimentVariant30 if bucket < experiment.controlPercentage {31 variant = .control32 } else {33 variant = .treatment34 }35 36 // Cache'le ve analytics'e kaydet37 assignments[experiment.rawValue] = variant.rawValue38 analytics.track("experiment_assigned", properties: [39 "experiment": experiment.rawValue,40 "variant": variant.rawValue41 ])42 43 return variant44 }45 46 private func stableHash(_ input: String) -> Int {47 var hash = 538148 for char in input.utf8 {49 hash = ((hash << 5) &+ hash) &+ Int(char)50 }51 return abs(hash)52 }53}54 55enum Experiment: String {56 case onboardingFlow = "onboarding_flow_v2"57 case checkoutRedesign = "checkout_redesign"58 case homeFeedAlgorithm = "home_feed_algo"59 60 var controlPercentage: Int {61 switch self {62 case .onboardingFlow: return 5063 case .checkoutRedesign: return 7064 case .homeFeedAlgorithm: return 8065 }66 }67}68 69enum ExperimentVariant: String, Codable {70 case control71 case treatment72}5. Kill Switch Mekanizması
Kill switch, production'daki bir özelliği anında devre dışı bırakmanızı sağlar. Ödeme sisteminiz çöktüğünde, üçüncü parti bir servis hata verdiğinde veya güvenlik açığı tespit ettiğinizde hayat kurtarır.
swift
1// MARK: - Kill Switch2struct KillSwitchMiddleware {3 static func check(4 feature: FeatureFlag,5 fallback: () -> Void,6 action: () -> Void7 ) {8 if FeatureFlagService.shared.isEnabled(feature) {9 action()10 } else {11 LogService.warning("Kill switch aktif: \(feature.rawValue)")12 fallback()13 }14 }15}16 17// Kullanim18// KillSwitchMiddleware.check(19// feature: .paymentSystemEnabled,20// fallback: { showMaintenanceScreen() },21// action: { proceedWithPayment() }22// )6. Flag Lifecycle Yönetimi
Feature flag'lerin en büyük tehlikesi: temizlenmemesi. Zamanla onlarca kullanılmayan flag birikir ve kod okunmaz hale gelir.
Lifecycle Kuralları
- Release flag: Özellik stable olduktan 1 sprint sonra kaldır
- Experiment flag: Sonuçlar analiz edildikten 2 hafta sonra kaldır
- Kill switch: Kalıcı olabilir ama yılda bir gözden geçir
- Ops flag: İhtiyaç oldukça kalabilir
swift
1extension FeatureFlag {2 var expiryDate: Date? {3 switch self {4 case .newProfilePage:5 return DateComponents(calendar: .current, year: 2026, month: 4, day: 1).date6 case .redesignedCheckout:7 return DateComponents(calendar: .current, year: 2026, month: 5, day: 15).date8 default: return nil // Kill switch ve ops flag'ler suresi dolmaz9 }10 }11 12 var owner: String {13 switch self {14 case .newProfilePage: return "profile-team"15 case .redesignedCheckout: return "payments-team"16 case .paymentSystemEnabled: return "platform-team"17 default: return "unassigned"18 }19 }20 21 // CI'da calistirin: FeatureFlag.warnExpired()22 static func warnExpired() {23 for flag in allCases {24 guard let expiry = flag.expiryDate, expiry < Date() else { continue }25 print("WARNING: Feature flag '\(flag.rawValue)' suresi dolmus! Owner: \(flag.owner)")26 }27 }28}ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Okuyucu Ödülü
Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:
Sonuç ve Öneriler
Feature flags modern iOS geliştirmenin temel taşlarından biridir. Doğru uygulandığında deployment riskini azaltır, A/B test yapmanızı sağlar ve production'da anlık kontrol verir. Ancak lifecycle yönetimini ihmal etmeyin — flag borcu, teknik borcun en sinsi halidir.

