Most apps treat configuration like a junk drawer.
Symptoms:
- constants scattered everywhere
- magic numbers in views
- flags mixed with secrets
- “just change it in prod” panic
- different behavior no one can explain
Configuration is not data.
It is control plane architecture.
This post shows how to design a clean, layered configuration system in SwiftUI that supports:
- remote config
- environment overrides
- safe defaults
- secret isolation
- predictable behavior across builds
🧠 The Core Principle
Configuration decides what the app is allowed to do —
not how it does it.
If config logic leaks into views, the system is already broken.
🧱 1. Configuration Is a First-Class Domain
Never do:
if UserDefaults.standard.bool(forKey: "new_ui") { ... }
Instead, define a domain model:
struct AppConfig {
let apiBaseURL: URL
let featureFlags: FeatureFlags
let experiments: Experiments
let limits: Limits
}
The app reads one config object.
📦 2. Configuration Sources (Layered)
A real app has multiple sources:
- Hardcoded defaults (safe baseline)
- Build-time config (Debug / Staging / Prod)
- Remote config (server-controlled)
- Local overrides (dev / QA only)
Each layer overrides the previous one.
🧭 3. Config Provider Architecture
protocol ConfigProvider {
func load() async throws -> AppConfig
}
Implementations:
- DefaultConfigProvider
- RemoteConfigProvider
- OverrideConfigProvider
Compose them:
final class AppConfigLoader {
func load() async -> AppConfig {
let base = DefaultConfig()
let remote = await RemoteConfig().merge(into: base)
let overridden = Overrides().merge(into: remote)
return overridden
}
}
One pipeline. Predictable behavior.
🔐 4. Secrets Are NOT Configuration
Never put secrets in:
- remote config
- plist files
- feature flags
- JSON
Secrets belong in:
- Keychain
- secure backend
- injected credentials
Config may reference capabilities, not secrets.
🧬 5. Typed Access Only
Bad:
config["max_items"]
Good:
config.limits.maxItems
This gives:
- compile-time safety
- refactorability
- documentation
🧪 6. Environment Overrides (Dev & QA)
Allow overrides only outside production:
#if DEBUG
config = config.withOverrides(localOverrides)
#endif
Rules:
- overrides never ship to prod
- overrides are visible in UI
- overrides are resettable
No hidden behavior.
⚠️ 7. Safe Defaults & Fallbacks
Remote config will fail sometimes.
Rules:
- defaults must be safe
- app must boot without network
- missing values must not crash
- unknown keys must be ignored
If config failure bricks the app, architecture failed.
🧠 8. Config Change Handling
Config changes should:
- apply at safe boundaries
- not rewire running features
- avoid mid-session flips
Correct:
- apply on next launch
- apply on feature entry
- apply on refresh boundaries
Never mutate live state arbitrarily.
❌ 9. Common Configuration Anti-Patterns
Avoid:
- reading config in views
- mixing config with business logic
- using config for authorization
- shipping debug toggles
- stringly-typed access
- no ownership or documentation
Config rot is real.
🧠 Mental Model
Think:
Defaults
→ Build Config
→ Remote Config
→ Local Overrides
→ App Behavior
Not:
“Just check this flag here”
🚀 Final Thoughts
A proper configuration system gives you:
- safe rollouts
- predictable environments
- faster debugging
- fewer hotfixes
- calmer releases
Configuration is infrastructure, not glue code.
Top comments (0)