How do you design feature flags and remote config for a mobile app: flag kinds, defaults, stable percentage rollouts, consistency, security and clean-up?

25.4.1 · L5 · Interview

Predict first

A rollout buckets users with SHA-256 and, for comparison, with hashValue. Across two runs, which stays the same?

// === main.swift ===
import Foundation
import CryptoKit

func bucket(_ user: String, _ flag: String) -> Int {
    let digest = SHA256.hash(data: Data((flag + ":" + user).utf8))
    let value = digest.prefix(4).reduce(UInt32(0)) { ($0 << 8) | UInt32($1) }
    return Int(value % 100)
}
print("bucket", bucket("user-42", "new-checkout"))
print("hash", "user-42".hashValue)
let inRollout = (0..<10_000).filter { bucket("user-\($0)", "new-checkout") < 10 }.count
print("rollout", inRollout > 850 && inRollout < 1150)
  • The SHA-256 bucket stays the same; hashValue changes; the 10% rollout reaches about 10%
  • Both stay the same
  • Neither stays the same

Mental model. A flag is a dimmer switch wired to a control room. You install the lamp in every house first, then raise the light house by house, and you can switch it off from the control room if it flickers.

A flag is a value fetched from a server (or compiled in) that code reads to decide behaviour. There are different kinds, with different lifetimes. Release flags hide unfinished or risky features and are removed after rollout. Experiment flags assign users to variants and are tied to a measured metric. Operational flags (kill switches) disable a feature or lower load in an incident and live longer. Permission flags unlock paid or beta features, which belong to the account and the server, not to a client-side toggle alone. The design questions are the same for all. Safe default: the app must behave sensibly with no network and no cached config, so every flag has a compiled-in default, usually “off” for a new feature and “on” for a kill switch’s healthy state. Freshness: fetch at launch and in the background, cache the last values, and decide when new values apply (immediately, or at the next launch, so a screen does not change under the user mid-flow). Keep evaluation consistent within a session and log the flag state with crashes and bug reports so you can tell which variant failed.

For percentage rollouts the assignment must be stable: hash a stable identifier (a user or install ID) with the flag name and take the result modulo 100, so the same user always lands in the same bucket and raising the percentage only adds users. Do not use Swift’s hashValue, because it is randomly seeded for each process launch (the check below shows it changes between runs while a SHA-256 based bucket does not, and the rollout reaches about the expected 10%). Security and hygiene: client flag values are visible to anyone who inspects traffic, so never put secrets in them and never rely on a client flag for authorisation; test both states of every flag in CI; give each flag an owner and an expiry, and delete the flag and the dead code path after rollout, because accumulated flags create combinations nobody tests (flag debt). Prefer a managed service or a well-tested in-house system; decide what happens when the service is down (use the cache, then the defaults). Document the process for turning a flag off during an incident, since that is what you built them for (Q432).

hashValue is randomly seeded per process, so it cannot be used for stable assignment.

Common traps

  • Using hashValue to bucket users. It changes every launch, so users flip in and out of the rollout.
  • Leaving flags in forever. Dead branches and untested combinations pile up; delete flags after rollout.

They might follow up with

  • How do you make a percentage rollout sticky? Hash a stable ID with the flag name and compare the bucket to the percentage.
  • What does the app do offline with no cached config? Uses the compiled-in safe defaults.

Related questions

  • 25.3.3 What can and cannot be rolled back after release, and how do you respond to a bad release: pause, flags…
  • 25.4.2 How do you run a release train for many apps: cadence, branch cuts, ownership, go/no-go criteria, hotfix…
  • 25.5.1 How do you make schema, API and data changes backward compatible: expand and contract, tolerant readers…
  • 2.1.2 What must Hashable guarantee, and why does iteration order change between launches?
  • 21.3.2 How do you build a performance culture on a team: budgets, regression gates, ownership and release guardrails?

How do you run a release train for many apps: cadence, branch cuts… →