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:

  1. Keep raw flags at the boundary.
  2. Convert them into one typed screen configuration.
  3. Make mutually exclusive states explicit.
  4. Add a kill-switch default when the combination is invalid.
  5. 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.