Feature Flags, Remote Config & Progressive Delivery: Shipping Risk, Not Just Code
- The Mobile Dilemma: Deployments Aren't Releases
- Feature Flags & Remote Config Explained
- Percentage-Based Staged Rollouts
- The Speed Hierarchy & The Kill-Switch
- ⚠️ Common Conflation: Staged Rollout vs. Kill Switch
- A/B Testing: Analytics Attribution
- Mini System Design: Building a Flag Service
- Interviewer's Follow-Up & Sample Answer
1. The Mobile Dilemma: Deployments Aren't Releases
In web development, if you deploy a broken checkout flow at 2:00 PM, you can usually revert the commit and push a fix by 2:05 PM. The web is inherently stateless in its distribution. Mobile development does not afford us this luxury.
When you hit "Publish" in the App Store or Google Play, you aren't just shipping code; you are handing over a compiled binary to a slow-moving, third-party distribution network. If that release contains a critical crash in the checkout flow, you face a structural nightmare:
- Review Bottlenecks: You have to submit a hotfix and wait for store review. Even expedited reviews take hours.
- Adoption Lag: Once approved, users don't update instantly. Many will continue using the broken version for days or weeks.
- The Point of No Return: A mobile release is not instantly reversible. Once it's in users' hands, you are stuck with the consequences.
This structural reality means that senior mobile developers don't just think about how to write clean code; they think about how to manage risk. The direct answer to this problem is decoupling the deployment of your code from the release of the feature.
Tap or hover bars to view mitigation speed
2. Feature Flags & Remote Config Explained
A feature flag (often managed via systems like Firebase Remote Config or LaunchDarkly) allows the app to check, at runtime, whether a specific feature should be enabled. The critical distinction is that this answer can be changed from a remote dashboard without requiring a new app release.
Let's use a consistent example: Your team has built a completely redesigned, highly optimized checkout flow for your shopping app. It looks great, but it interacts with a new payment gateway, making it inherently risky.
Instead of hardcoding the navigation to the new screen, you wrap the entry point in a flag condition. At a whiteboard level, an interviewer expects you to describe this mechanism:
- Fetch & Cache: On app launch (or periodically), the app fetches a JSON payload of key-value pairs from the remote config server and caches them locally.
- Safe Fallbacks: If the user is on a subway with no internet, the fetch fails. The system must have safe, hardcoded default values so the app doesn't crash or block execution while waiting for the network.
- Conditional Execution: At the critical decision point, the app asks the local cache for the flag value.
Future<void> navigateToCheckout(BuildContext context) async {
// 1. Retrieve the cached value (does not block on network)
final bool isNewCheckoutEnabled = remoteConfigService.getBool(
'enable_v2_checkout',
defaultValue: false, // 2. Safe fallback
);
// 3. Conditional routing
if (isNewCheckoutEnabled) {
Navigator.of(context).push(NewCheckoutFlow.route());
} else {
Navigator.of(context).push(LegacyCheckoutFlow.route());
}
}
3. Percentage-Based Staged Rollouts
Flipping a flag from 0% to 100% for everyone simultaneously is better than a hardcoded release, but it still subjects your entire user base to massive, sudden risk. This is where Progressive Delivery comes in.
Rather than a simple binary on/off state, a robust feature flag system allows you to enable the flag for a small percentage of your audience—say, 5%. Why does this matter? Blast radius reduction.
Imagine your new checkout flow has a subtle concurrency bug that only reveals itself under heavy, real-world traffic patterns, causing payments to double-charge. If you launched to 100% of users, you have a company-ending crisis. If you launch to 5% of users, you limit the blast radius to a small cohort. Your monitoring tools (Crashlytics, Datadog) will still catch the spike in errors or the drop in conversion rates, allowing your team to investigate while 95% of your revenue stream remains protected on the legacy flow.
4. The Speed Hierarchy & The Kill-Switch
You'll want this pattern the first time a release goes wrong at 2 AM. The single most powerful use case for a feature flag is the Kill-Switch.
When the 5% canary rollout mentioned above starts throwing severe payment errors, what is the fastest way to stop the bleeding? You do not write a fix. You do not open Xcode or Android Studio. You log into your remote config dashboard and change `enable_v2_checkout` to 0%.
Within minutes, as app instances re-fetch their config (or receive silent push notifications to invalidate cache), the new checkout flow disappears. The app reverts entirely to the legacy, stable code path. This mitigation is vastly faster than scrambling to build, sign, upload, and beg Apple/Google for an expedited hotfix review.
5. ⚠️ Common Conflation: Staged Rollout vs. Kill Switch
In senior interviews, candidates frequently conflate a Store-Level Staged Rollout with an App-Level Feature Flag / Kill Switch. Describing them as the same thing is a massive red flag that reveals a lack of practical release experience. They are two different, complementary layers of defense.
- Mechanism: Controlled via Google Play Console or App Store Connect.
- Scope: Controls which users receive a specific app binary (v2.1.0).
- Reversibility: You can pause a rollout to stop new users from getting the bad build, but you cannot revert the users who already downloaded it. They are stuck until a new hotfix binary is released.
- Mechanism: Controlled via a backend service (Firebase, LaunchDarkly).
- Scope: Controls whether a feature is active within a build that is already installed on the user's device.
- Reversibility: Can instantly flip off a broken feature for all users, returning them to a stable code path without any app store interaction.
Use store-level rollouts to protect against native crashes, memory leaks, and core startup bugs. Use feature flags to protect against business-logic errors, API contract changes, and UI regressions like our checkout example.
6. A/B Testing: Analytics Attribution
A feature flag controls what users see. But if you don't connect that flag to your analytics, it tells you nothing about whether the change actually helped. If overall revenue drops by 2% during your checkout rollout, how do you know if the new flow is responsible, or if it's just a slow sales day?
When a flag value resolves for a given user, that assignment must be recorded alongside subsequent analytics events. This is called event tagging or experiment attribution.
Every time the user completes a purchase, the event payload must carry the state of the flag. This allows product teams to query the data later: "Compare the conversion rate of users who had enable_v2_checkout: true versus those who had enable_v2_checkout: false."
Future<void> logPurchase(Order order) async {
// Retrieve current flag state
final bool isNewCheckout = remoteConfig.getBool('enable_v2_checkout');
// Tag the assignment into the analytics payload
await analytics.logEvent(
name: 'ecommerce_purchase',
parameters: {
'transaction_id': order.id,
'value': order.totalPrice,
'checkout_variant': isNewCheckout ? 'v2_flow' : 'legacy_flow', // CRITICAL
},
);
}
7. Mini System Design: Building a Flag Service
If an interviewer asks, "How would you design our own internal feature flag system from scratch?", they are looking for three conceptual pillars:
- Fetch, Cache & Fallback: The app must not make synchronous network calls on UI thread to check flags. A background isolate or startup sequence should fetch the JSON payload, cache it in local storage (like SharedPreferences), and always evaluate against the cache. The API must mandate a default fallback value.
- Consistent Hashing for Assignment: If a user is assigned to the 50% rollout group, they must remain in that group across app restarts. You do this locally using a deterministic hash function (like MurmurHash). Hash the user's UUID combined with the flag name. If the result modulo 100 is less than the target percentage, they are in. This requires zero server-side state tracking.
- Analytics Abstraction: The flag evaluation engine should automatically hook into the analytics service, emitting a `flag_evaluated` event the moment the code checks the flag, ensuring attribution is never forgotten by the developer.
Q: What's the difference between a staged rollout and a kill switch?
A: Staged rollouts (App Store level) control which users get a binary; pausing it doesn't fix users who already downloaded it. A kill switch (Remote Config) controls logic inside an existing binary; flipping it instantly reverts everyone to a safe state.
Q: How do you prevent flag checks from slowing down app startup?
A: Never block the UI waiting for a network fetch. Read from the local cache synchronously, and fetch the latest config asynchronously in the background for the next app launch.
8. Senior-Level Sample Answer
If asked: "We are replacing our legacy checkout with a brand new flow. How would you safely roll this out to our users?"
Before moving to the next post, answer this unscripted: If a user loses network connectivity right as they open your app, how does your app know which version of the checkout flow to show them?

Comments
Post a Comment