androidengineers.Book a session

Scalable Architecture

Feature Flags & Remote Config

article20 minMedium

Feature flags decouple deployment from release. You ship code that's turned off, then toggle it on — for specific users, percentages of traffic, or in response to incidents — without an app update.

Why Feature Flags Matter

ScenarioWithout flagsWith flags
Risky new featureBig bang launch, hard to roll backGradual rollout; 1% → 10% → 100%
Incident responseHotfix + emergency releaseToggle off via remote config in seconds
A/B testingManual variants, hard to measureServer-controlled; automatic analytics
Per-tenant featuresMultiple buildsOne build; flags per tenant

Firebase Remote Config

Firebase Remote Config is the most common Android flag solution:

// 1. Define defaults (values used before remote fetch)
val defaults = mapOf(
    "new_feed_algorithm" to false,
    "max_article_length" to 5000L,
    "chat_enabled" to false
)

// 2. Initialize in Application.onCreate
val remoteConfig = Firebase.remoteConfig.apply {
    setDefaultsAsync(defaults)
    setConfigSettingsAsync(remoteConfigSettings {
        minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 3600
    })
}

// 3. Fetch and activate
class FeatureFlagManager(private val remoteConfig: FirebaseRemoteConfig) {
    suspend fun fetchAndActivate() {
        remoteConfig.fetchAndActivate().await()
    }

    val isNewFeedEnabled: Boolean
        get() = remoteConfig.getBoolean("new_feed_algorithm")

    val maxArticleLength: Long
        get() = remoteConfig.getLong("max_article_length")

    val isChatEnabled: Boolean
        get() = remoteConfig.getBoolean("chat_enabled")
}

Feature Flag Abstraction

Don't scatter remoteConfig.getBoolean(...) calls throughout your code. Create a typed abstraction:

interface FeatureFlags {
    val isNewFeedEnabled: Boolean
    val isChatEnabled: Boolean
    val maxArticleLength: Int
}

class FirebaseFeatureFlags(
    private val remoteConfig: FirebaseRemoteConfig
) : FeatureFlags {
    override val isNewFeedEnabled get() = remoteConfig.getBoolean("new_feed_algorithm")
    override val isChatEnabled get() = remoteConfig.getBoolean("chat_enabled")
    override val maxArticleLength get() = remoteConfig.getLong("max_article_length").toInt()
}

// In tests — control all flags:
class TestFeatureFlags(
    override val isNewFeedEnabled: Boolean = false,
    override val isChatEnabled: Boolean = false,
    override val maxArticleLength: Int = 5000
) : FeatureFlags

Using Flags in Code

class FeedViewModel(
    private val repository: FeedRepository,
    private val flags: FeatureFlags
) : ViewModel() {

    fun loadFeed() = viewModelScope.launch {
        val articles = if (flags.isNewFeedEnabled) {
            repository.getPersonalizedFeed()
        } else {
            repository.getChronologicalFeed()
        }
        _state.value = UiState.Success(articles)
    }
}

// In Compose:
@Composable
fun FeedScreen(flags: FeatureFlags = get()) {
    if (flags.isChatEnabled) {
        ChatFab(modifier = Modifier.align(Alignment.BottomEnd))
    }
}

Gradual Rollout Strategy

// Firebase Remote Config supports conditions:
// - User property: "country = US"
// - App version: "version >= 5.0"
// - Random percentile: "Random 10%" (for 10% rollout)

// In Firebase Console → Remote Config → Add condition:
// Name: "10% early adopters"
// Condition: "User in Random Percentile" 0–10%
// Value for this condition: new_feed_algorithm = true

Kill Switch

The most important flag is a kill switch for a production incident:

class ArticleDetailViewModel(
    private val flags: FeatureFlags,
    private val repository: ArticleRepository
) : ViewModel() {

    fun loadComments(articleId: String) {
        if (!flags.isCommentsEnabled) {
            // Comment system is having issues — show empty state
            _commentsState.value = CommentsState.Disabled
            return
        }
        // ... load comments
    }
}

When an incident fires: toggle comments_enabled = false in Firebase console. Comments disappear for all users within the next fetch interval (~1 hour by default, or immediately with a force fetch).

Testing Feature Flags

@Test
fun `shows personalized feed when new algorithm enabled`() {
    val flags = TestFeatureFlags(isNewFeedEnabled = true)
    val viewModel = FeedViewModel(mockRepository, flags)

    viewModel.loadFeed()

    verify(mockRepository).getPersonalizedFeed()
}

@Test
fun `shows chronological feed when new algorithm disabled`() {
    val flags = TestFeatureFlags(isNewFeedEnabled = false)
    val viewModel = FeedViewModel(mockRepository, flags)

    viewModel.loadFeed()

    verify(mockRepository).getChronologicalFeed()
}

Key Takeaways

ConceptRule
Abstract flagsCreate a FeatureFlags interface; never scatter getBoolean calls in business logic
Test overridesTestFeatureFlags in unit tests; no network calls in tests
Kill switchEvery major feature should have one; practice the incident response flow
Fetch interval1 hour in production; 0 in debug for rapid iteration
Gradual rolloutStart at 1%; monitor crash rates and ANRs before increasing

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Feature Flags & Remote Config | Android System Design | Android Engineers