Setting up Feature Flags in mobile app
Feature flags — mechanism for enabling and disabling features without app update. Sounds minor, but changes entire development process: trunk-based development becomes possible, release train separates from feature readiness, and you have kill switch if new feature breaks production.
Most common use — postmortem protection: feature in production breaks payment flow for 5% of users, team can't ship hotfix in 2 hours due to App Store review. Flag disabled in 30 seconds.
Firebase Remote Config as basic tool
Remote Config — simplest way to implement flags without additional SDKs:
// iOS
let remoteConfig = RemoteConfig.remoteConfig()
// Default values — work offline and until first fetch
remoteConfig.setDefaults([
"new_payment_flow_enabled": false as NSObject,
"chat_feature_enabled": false as NSObject,
"max_cart_items": 50 as NSObject
])
remoteConfig.fetch(withExpirationDuration: 300) { status, error in // 5 minutes
remoteConfig.activate()
}
// Usage
var isNewPaymentEnabled: Bool {
remoteConfig.configValue(forKey: "new_payment_flow_enabled").boolValue
}
// Android
val remoteConfig = Firebase.remoteConfig
remoteConfig.setDefaultsAsync(mapOf(
"new_payment_flow_enabled" to false,
"chat_feature_enabled" to false
))
remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
val isNewPaymentEnabled = remoteConfig.getBoolean("new_payment_flow_enabled")
}
Main Firebase Remote Config downside — no flexible targeting. Flag on for everyone or for segment by conditions (country, app version, Firebase audience). Percentage rollout exists but through A/B Testing interface.
LaunchDarkly for complex targeting
When need rollout by specific user_id, companies or complex rules:
// iOS LaunchDarkly SDK
import LaunchDarkly
let user = LDUser(key: userId, email: userEmail)
LDClient.start(config: LDConfig(mobileKey: "mob-xxx"), user: user)
// Synchronous flag check
let isEnabled = LDClient.shared.boolVariation(forKey: "new_payment_flow", defaultValue: false)
// With context for logging
let (value, detail) = LDClient.shared.boolVariationDetail(forKey: "new_payment_flow", defaultValue: false)
print("Reason: \(detail.reason)") // RULE_MATCH, FALLTHROUGH, OFF...
LaunchDarkly supports targeting rules: enable flag for users with plan == "enterprise" or for first 10% of users sorted by user_id. For gradual rollout this is more accurate than random percentage.
Flag organization in code
All flags in one place — not scattered through business logic:
// FeatureFlags.swift
struct FeatureFlags {
private let remoteConfig = RemoteConfig.remoteConfig()
var isNewPaymentFlowEnabled: Bool {
remoteConfig.configValue(forKey: "new_payment_flow_enabled").boolValue
}
var isChatEnabled: Bool {
remoteConfig.configValue(forKey: "chat_feature_enabled").boolValue
}
var maxCartItems: Int {
Int(remoteConfig.configValue(forKey: "max_cart_items").numberValue)
}
}
// Usage
if AppDependencies.featureFlags.isNewPaymentFlowEnabled {
showNewPaymentFlow()
} else {
showLegacyPaymentFlow()
}
If flags centralized, finding all uses through grep or refactor — task of minutes, not hours.
Flag lifecycle: creation → deletion
Flags accumulate and become technical debt. Good practice — flag lives max 3 months:
- Flag created — feature hidden
- Rollout started — enable gradually
- 100% of users on new version → flag = true for everyone
- Delete flag and dead code of old behavior from codebase
If not deleting — in a year codebase will have 40 flags, half of which true for 100% of audience.
What's included in the work
- Remote Config (Firebase) or LaunchDarkly / Statsig connection
- Implementing centralized
FeatureFlagslayer - Setting up default values for offline work
- Configuring targeting rules and rollout percentages
- Setting up flag monitoring in dashboard
- Documenting flag lifecycle for team
Timeline
Firebase Remote Config with basic flag set: 0.5–1 day. LaunchDarkly with targeting rules and percentage rollout: 1–2 days. Cost calculated individually.







