What the problem actually is
The screen is controlled by four remote flags.
One enables a new layout.
One enables a promo banner.
One disables an old fallback.
One turns on a new loading experience.
Each flag makes sense alone.
Together they can produce UI states nobody planned:
- empty screens with no fallback
- analytics events from a branch that is visually hidden
- a CTA that appears without the data required to support it
- a rollout that cannot be reasoned about from product or QA
This is not a “remote config edge case”.
It is an invalid state model.
Why it keeps happening
Teams often treat flags as harmless booleans sprinkled across view code.
That feels fast, but it spreads rollout logic into many layers:
- data loading
- view models
- UI composition
- analytics
- experiment tracking
Then no single place can answer:
What are the valid product states of this screen?
Without that answer, impossible combinations are inevitable.
The implementation boundary that matters
The important boundary is between raw flags and screen configuration.
Raw flags are inputs.
The app should not render directly from raw flags in five different places.
Instead, one owner should derive a valid configuration or reject the combination.
That configuration should represent states the product actually supports.
A concrete pattern to fix it
The pattern I trust is:
- Keep raw flags at the boundary.
- Convert them into one typed screen configuration.
- Make mutually exclusive states explicit.
- Add a kill-switch default when the combination is invalid.
- Remove expired flags aggressively.
This is simplified pseudocode, not production code.
struct RawFlags {
let newLayoutEnabled: Bool
let promoBannerEnabled: Bool
let legacyFallbackDisabled: Bool
let modernLoadingEnabled: Bool
}
enum ScreenConfiguration {
case legacy
case modernWithBanner
case modernWithoutBanner
case safeFallback
}
func configuration(from flags: RawFlags) -> ScreenConfiguration {
if flags.newLayoutEnabled {
if flags.promoBannerEnabled {
return .modernWithBanner
}
return .modernWithoutBanner
}
if flags.legacyFallbackDisabled {
return .safeFallback
}
return .legacy
}
The point is not the exact enum.
The point is that the UI now depends on one configuration model instead of many scattered branches.
How to verify the fix
Verification should cover behavior, not just visuals.
Check:
- every supported configuration renders correctly
- analytics events align with the visible state
- disabled combinations land in the safe fallback
- QA can describe the rollout in terms of configurations, not raw booleans
This is a good place for small matrix tests because the problem is combinatorial by nature.
What still goes wrong in production
One common mistake is computing configuration more than once in different layers. Then analytics, data loading, and UI can each believe a different state is active.
Another is keeping dead flags around after the rollout. Old branches create future confusion even when the feature is “done”.
The third is treating kill switches as optional. If the flag system cannot produce a safe fallback, it is not ready for high-stakes rollout use.
The stable contract is:
Flags are raw input. Product states are explicit. UI renders from one owned configuration.
That is what prevents feature flags from creating impossible mobile states.