DEV Community

Sebastien Lato
Sebastien Lato

Posted on

SwiftUI Feature Flag Architecture (Kill Switches, Rollbacks, Safety)

Feature flags are not just for experiments.

In production apps, they are:

  • safety nets
  • kill switches
  • rollback mechanisms
  • rollout controls
  • architectural boundaries

Most teams misuse flags and end up with:

  • permanent flags
  • unreadable conditionals
  • hidden behavior
  • impossible debugging
  • “just one more flag” syndrome

This post shows how to design a clean, safe feature flag architecture in SwiftUI that:

  • protects production
  • scales with teams
  • stays maintainable
  • doesn’t pollute your UI

🧠 The Core Principle

Feature flags control availability, not behavior.

If a flag changes how a feature works, your architecture is already broken.


🧱 1. Flags Are Data, Not Logic

Never do this in views:

if flags.newProfileEnabled {
    NewProfileView()
} else {
    OldProfileView()
}
Enter fullscreen mode Exit fullscreen mode

This scatters flag logic everywhere.


✅ Correct Pattern: Centralized Flag Evaluation

enum Feature {
    case newProfile
    case advancedSearch
    case aiSuggestions
}
Enter fullscreen mode Exit fullscreen mode
protocol FeatureFlags {
    func isEnabled(_ feature: Feature) -> Bool
}
Enter fullscreen mode Exit fullscreen mode

Views never know why something is enabled.


📦 2. Flag Provider Architecture

final class FeatureFlagService: FeatureFlags {
    private let config: RemoteConfig

    func isEnabled(_ feature: Feature) -> Bool {
        switch feature {
        case .newProfile:
            return config.bool("new_profile")
        case .advancedSearch:
            return config.bool("advanced_search")
        case .aiSuggestions:
            return config.bool("ai_suggestions")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This allows:

  • remote control
  • overrides
  • testing
  • rollbacks

🧭 3. Flags Decide Feature Entry, Not Internals

Correct:

func buildProfile() -> some View {
    if flags.isEnabled(.newProfile) {
        NewProfileFeature()
    } else {
        LegacyProfileFeature()
    }
}
Enter fullscreen mode Exit fullscreen mode

Incorrect:

if flags.isEnabled(.newProfile) {
    doThingA()
} else {
    doThingB()
}
Enter fullscreen mode Exit fullscreen mode

Flags choose which feature exists, not how it behaves.


🔥 4. Kill Switches

Every risky feature must have:

  • a single flag
  • default OFF
  • server-side control

Example use cases:

  • crashes
  • data corruption
  • backend instability

Kill switches should:

  • bypass entire features
  • fail safe
  • never require a client update

🧬 5. Environment Overrides

Support:

  • local overrides
  • QA toggles
  • developer testing
FeatureFlagService(
    remote: remoteConfig,
    overrides: localOverrides
)
Enter fullscreen mode Exit fullscreen mode

Never ship dev overrides to production.


🧪 6. Testing with Flags

final class MockFlags: FeatureFlags {
    let enabled: Set<Feature>

    func isEnabled(_ feature: Feature) -> Bool {
        enabled.contains(feature)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now you can test:

  • enabled paths
  • disabled paths
  • rollout safety

Flags must be testable.


⚠️ 7. Flag Lifecycle Rules

Every flag must have:

  • owner
  • purpose
  • expiration date
  • removal plan

Flags are temporary scaffolding, not architecture.


❌ 8. Common Feature Flag Anti-Patterns

Avoid:

  • flags inside views
  • nested flag checks
  • behavior-altering flags
  • permanent flags
  • undocumented flags
  • flags replacing architecture

Flags should disappear over time.


🧠 Mental Model

Think:

Flag
  Feature Exists?
    Route / Entry Point
      Normal Architecture
Enter fullscreen mode Exit fullscreen mode

Not:

“Sprinkle if-statements everywhere”


🚀 Final Thoughts

A correct feature flag system gives you:

  • safe rollouts
  • instant kill switches
  • fearless releases
  • controlled experimentation
  • production stability

Bad flags rot codebases.
Good flags protect them.

Top comments (0)