How-To: Load Configuration Safely from Remote
Problem
You need to:
- Load feature flag configuration from a remote source (API, S3, CDN)
- Validate configuration before applying it to production traffic
- Handle invalid configuration without breaking the service
- Update configuration without redeploying code
Solution
Step 1: Define Features Statically
object AppFeatures : Namespace("app") {
val darkMode by boolean<Context>(default = false)
val maxRetries by integer<Context>(default = 3)
val checkoutFlow by enum<CheckoutVariant, Context>(default = CheckoutVariant.CLASSIC)
}
Static definitions establish the contract: Types, keys, and defaults are known at compile-time.
Step 2: Load Configuration with Explicit Validation
fun loadRemoteConfiguration() {
val json = try {
httpClient.get("https://config.example.com/app-features.json").body<String>()
} catch (e: Exception) {
logger.error("Failed to fetch remote config", e)
// Last-known-good remains active
return
}
when (val result = NamespaceSnapshotLoader(AppFeatures).load(json)) {
is ParseResult.Success -> {
logger.info("Remote config loaded successfully")
metrics.increment("config.load.success")
}
is ParseResult.Failure -> {
logger.error("Remote config validation failed: ${result.error}")
metrics.increment("config.load.failure")
alertOps("Configuration validation failed", result.error)
// Last-known-good remains active
}
}
}
Key insight: ParseResult makes validation explicit. Invalid config is rejected before affecting traffic.
Step 3: Handle Parse Failures Gracefully
when (val result = NamespaceSnapshotLoader(AppFeatures).load(json)) {
is ParseResult.Failure -> {
when (result.error) {
is ParseError.InvalidJSON -> {
logger.error("JSON syntax error: ${result.error.message}")
// Alert: config server is returning malformed JSON
}
is ParseError.UnknownFeature -> {
logger.error("Unknown feature key: ${result.error.key}")
// Alert: config references a feature that doesn't exist in code
}
is ParseError.TypeMismatch -> {
logger.error("Type mismatch for ${result.error.key}: expected ${result.error.expectedType}, got ${result.error.actualType}")
// Alert: config has wrong type for a feature
}
}
}
}
Step 4: Use Initial Defaults if Remote Unavailable
class ConfigurationManager(private val namespace: Namespace) {
private var initialized = false
fun initialize() {
if (initialized) return
// Try to load remote config
val loaded = try {
val json = fetchRemoteConfig()
when (val result = NamespaceSnapshotLoader(namespace).load(json)) {
is ParseResult.Success -> {
logger.info("Initialized with remote config")
true
}
is ParseResult.Failure -> {
logger.warn("Remote config invalid: ${result.error}")
false
}
}
} catch (e: Exception) {
logger.warn("Failed to fetch remote config on init", e)
false
}
if (!loaded) {
logger.info("Using default configuration")
}
initialized = true
// Service starts either way—with remote config or defaults
}
}
Guarantees
-
Validation at boundary: Invalid config rejected before affecting traffic
- Mechanism:
ParseResult.Failurereturned if JSON doesn't match definitions - Boundary: Validation catches schema errors, not business logic errors
- Mechanism:
-
Atomic replacement: All evaluations see old config OR new config, never partial
- Mechanism: Configuration atomically swapped on successful load
- Boundary: No guarantee about when a particular request sees the update
-
Last-known-good preserved: Failed loads don't affect evaluation
- Mechanism: Failed load doesn't modify namespace state
- Boundary: "Last-known-good" might be initial defaults if no successful load
Configuration Format
Valid JSON Example
{
"darkMode": {
"rules": [
{
"value": true,
"predicates": {
"platforms": ["IOS", "ANDROID"]
}
}
]
},
"maxRetries": {
"rules": [
{
"value": 5,
"predicates": {
"android": true
}
}
]
},
"checkoutFlow": {
"rules": [
{
"value": "SIMPLIFIED",
"predicates": {
"rampUp": { "percentage": 50.0 }
}
}
]
}
}
What Gets Validated
- JSON syntax: Must be valid JSON
- Feature keys: Must match defined properties in namespace
- Type safety: Values must match feature types
- Rule structure: Rules must have valid predicates
See Persistence Format for complete schema.
What Can Go Wrong?
Network Failures
// Config fetch times out or fails
try {
val json = httpClient.get(configUrl).body<String>()
} catch (e: Exception) {
// DON'T: Crash the service
// DO: Log error, keep last-known-good, alert ops
logger.error("Config fetch failed", e)
metrics.increment("config.fetch.failure")
}
Result: Service continues with last-known-good configuration.
Typo in Feature Key
{
"darkMood": { // Typo: should be "darkMode"
"rules": [{ "value": true }]
}
}
Result: ParseResult.Failure(UnknownFeature("darkMood")). Config rejected, last-known-good preserved.
Type Mismatch
{
"maxRetries": {
"rules": [{ "value": "five" }] // Wrong type: String instead of Int
}
}
Result: ParseResult.Failure(TypeMismatch("maxRetries", expectedType = "Int", actualType = "String")). Config
rejected.
Partial Configuration
{
"darkMode": {
"rules": [{ "value": true }]
}
// maxRetries and checkoutFlow not included
}
Result: ParseResult.Success. Only darkMode is overridden. maxRetries and checkoutFlow use their static
definitions.
Best practice: Partial configuration is fine for gradual rollouts. Features not in JSON use static rules + defaults.
Advanced Patterns
Pattern: Versioned Configuration
data class VersionedConfig(
val version: String,
val config: String,
val timestamp: Instant
)
class ConfigLoader(private val namespace: Namespace) {
private var currentVersion: String? = null
fun loadVersioned(versioned: VersionedConfig) {
when (val result = NamespaceSnapshotLoader(namespace).load(versioned.config)) {
is ParseResult.Success -> {
logger.info("Loaded config version ${versioned.version}")
currentVersion = versioned.version
metrics.gauge("config.version", versioned.version)
}
is ParseResult.Failure -> {
logger.error("Config version ${versioned.version} invalid: ${result.error}")
metrics.increment("config.invalid_version", tags = mapOf(
"version" to versioned.version
))
}
}
}
fun getCurrentVersion(): String? = currentVersion
}
Pattern: Staged Rollout
class StagedConfigLoader(private val namespace: Namespace) {
fun loadWithCanary(json: String, canaryPercentage: Double = 1.0) {
// First: validate without loading
when (val result = NamespaceSnapshotLoader(namespace).validate(json)) {
is ParseResult.Failure -> {
logger.error("Validation failed: ${result.error}")
return
}
}
// Second: apply to canary traffic only
if (Random.nextDouble() < canaryPercentage / 100.0) {
NamespaceSnapshotLoader(namespace).load(json)
logger.info("Config applied to canary traffic")
}
// Third: after monitoring, apply to all traffic
// (This is a simplified example; real implementation would be more sophisticated)
}
}
Pattern: Configuration Diff
fun logConfigDiff(oldJson: String, newJson: String) {
val oldConfig = Json.parseToJsonElement(oldJson).jsonObject
val newConfig = Json.parseToJsonElement(newJson).jsonObject
val added = newConfig.keys - oldConfig.keys
val removed = oldConfig.keys - newConfig.keys
val modified = newConfig.keys.intersect(oldConfig.keys).filter {
oldConfig[it] != newConfig[it]
}
logger.info("""
Config diff:
Added: $added
Removed: $removed
Modified: $modified
""".trimIndent())
}
Monitoring Remote Configuration
Metrics to Track
// Load success/failure rate
metrics.increment("config.load.success")
metrics.increment("config.load.failure")
// Validation failure reasons
metrics.increment("config.validation.type_mismatch")
metrics.increment("config.validation.unknown_feature")
metrics.increment("config.validation.invalid_json")
// Load latency
metrics.recordLatency("config.load.duration", duration)
// Configuration version
metrics.gauge("config.version", version)
Alerts to Configure
- No successful load in X minutes: Remote config source may be down
- Validation failure rate > threshold: Config server is sending bad data
- Fetch failures spike: Network issues or config server issues
- Version hasn't changed in X hours: Config pipeline may be stuck
Testing Remote Configuration
Test Invalid JSON Rejection
@Test
fun `invalid JSON is rejected`() {
val invalidJson = """{ "darkMode": { "rules": [ INVALID ] } }"""
val result = NamespaceSnapshotLoader(AppFeatures).load(invalidJson)
assertTrue(result is ParseResult.Failure)
assertTrue((result as ParseResult.Failure).error is ParseError.InvalidJSON)
}
Test Type Safety
@Test
fun `type mismatch is rejected`() {
val json = """{ "maxRetries": { "rules": [{ "value": "five" }] } }"""
val result = NamespaceSnapshotLoader(AppFeatures).load(json)
assertTrue(result is ParseResult.Failure)
val error = (result as ParseResult.Failure).error
assertTrue(error is ParseError.TypeMismatch)
}
Test Partial Configuration
@Test
fun `partial configuration loads successfully`() {
val json = """{ "darkMode": { "rules": [{ "value": true }] } }"""
val result = NamespaceSnapshotLoader(AppFeatures).load(json)
assertTrue(result is ParseResult.Success)
// darkMode overridden
val ctx = Context(stableId = StableId("user"))
assertTrue(AppFeatures.darkMode.evaluate(ctx))
// maxRetries still uses default
assertEquals(3, AppFeatures.maxRetries.evaluate(ctx))
}
Next Steps
- Refresh Patterns — Polling, webhooks, file watch
- Failure Modes — Comprehensive error scenarios
- Handling Failures — What to do when config fails
- Persistence Format — Complete JSON schema