Skip to main content
Version: v0.13

Cloud Configuration Providers

Cloud configuration providers fetch configuration from remote sources, enabling centralized configuration management, dynamic updates, and multi-environment support.

Available Providers

ProviderModulePlatformUse Case
REST Clientconfig-rest-clientMultiplatformGeneric REST config servers
Azure App Configurationazure-app-configJVMAzure cloud deployments

CloudConfigProvider Interface

All cloud providers implement the CloudConfigProvider interface:

interface CloudConfigProvider : PropertySource<Map<String, Any>> {
val providerId: String
val isAvailable: Boolean
val configLevel: ConfigLevel
val lastRefreshedAt: Instant?

suspend fun refresh(): IdkResult<RefreshResult, IdkError>
fun watchForChanges(): Flow<ConfigChangeEvent>
suspend fun startWatching()
suspend fun stopWatching()
suspend fun healthCheck(): IdkResult<CloudProviderHealth, IdkError>
}

Data Classes

data class RefreshResult(
val configCount: Int, // Number of configuration keys loaded
val changedKeys: Set<String>, // Keys that changed since last refresh
val refreshedAt: Instant // Timestamp of refresh
)

data class ConfigChangeEvent(
val changedKeys: Set<String>,
val source: String,
val timestamp: Instant
)

data class CloudProviderHealth(
val isHealthy: Boolean,
val latencyMs: Long,
val configKeyCount: Int,
val lastSuccessfulRefresh: Instant?,
val errorMessage: String?
)

REST Configuration Client

The config-rest-client module provides a multiplatform REST API client for fetching configuration from remote servers.

Installation

// build.gradle.kts
dependencies {
implementation(project(":lib-conf-config-rest-client"))
implementation(project(":lib-conf-config-offline-cache")) // Optional
}

Configuration

data class RestConfigProviderConfig(
val baseUrl: String,
val tenantId: String,
val settingsPath: String = "/api/v1/config/settings",
val refreshPath: String = "/api/v1/config/refresh",
val auth: RestAuthConfig? = null,
val connectTimeoutMs: Long = 5000,
val readTimeoutMs: Long = 30000,
val refreshIntervalMs: Long = 60000, // 1 minute
val offlineCacheEnabled: Boolean = true,
val baseConfig: CloudConfigProviderConfig = CloudConfigProviderConfig()
)

Authentication Options

val config = RestConfigProviderConfig(
baseUrl = "https://config.mycompany.com",
tenantId = "my-tenant",
auth = RestAuthConfig.BearerToken("my-jwt-token")
)

Basic Usage

val provider = RestConfigProviderImpl(
config = config,
httpClient = HttpClient(CIO),
offlineCache = KmpSettingsStorage()
)

// Initial load
val refreshResult = provider.refresh()
if (refreshResult.isOk) {
println("Loaded ${refreshResult.value.configCount} configuration keys")
}

// Access properties
val dbHost = provider.getProperty("database.host", String::class)
val timeout = provider.getProperty("http.timeout", Int::class, 5000)

API Compatibility

The REST client expects these endpoints:

MethodPathDescription
GET/api/v1/config/settingsList settings
GET/api/v1/config/settings/{key}Get setting by key
POST/api/v1/config/refreshTrigger cache refresh

Query Parameters:

ParameterTypeDescription
scopeStringAPP, TENANT, or PRINCIPAL
profileStringConfiguration profile
keyPrefixStringFilter by key prefix

Azure App Configuration

The azure-app-config module provides integration with Azure App Configuration service.

Installation

// build.gradle.kts
dependencies {
implementation(project(":lib-conf-azure-app-config"))

// Azure SDK
implementation(platform("com.azure:azure-sdk-bom:1.2.18"))
implementation("com.azure:azure-data-appconfiguration")
implementation("com.azure:azure-identity")
}

Configuration

data class AzureAppConfigProviderConfig(
val connectionString: String? = null,
val endpoint: String? = null,
val credentialOptions: AzureCredentialOptions? = null,
val keyFilter: String = "*",
val labelFilter: String? = null,
val refreshInterval: Duration = 30.seconds,
val sentinelKey: String? = null,
val keyPrefix: String? = null,
val trimKeyPrefix: Boolean = true,
val offlineCacheEnabled: Boolean = true,
val tenantId: String? = null,
val baseConfig: CloudConfigProviderConfig = CloudConfigProviderConfig()
)

Authentication Methods

// Recommended for production - uses DefaultAzureCredential
val config = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
credentialOptions = AzureCredentialOptions(
authMethod = AzureAuthMethod.DEFAULT_CREDENTIAL
),
labelFilter = "production"
)

Label-Based Environment Separation

Azure App Configuration uses labels to separate environments:

// Production environment
val prodConfig = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
labelFilter = "production"
)

// Staging environment
val stagingConfig = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
labelFilter = "staging"
)

// No label (default values)
val defaultConfig = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
labelFilter = null // or "\\0" for no label
)

Key Conventions

Keys are parsed to determine their scope:

Key FormatScopeExample
{key}APPdatabase.host
tenant.{tenantId}.{key}TENANTtenant.acme.feature.enabled
principal.{tenantId}.{principalId}.{key}PRINCIPALprincipal.acme.alice.theme

Change Watching

Both providers support watching for configuration changes.

Polling-Based Watching

// Start watching (uses configured refresh interval)
provider.startWatching()

// Collect change events
provider.watchForChanges().collect { event ->
println("Config changed: ${event.changedKeys}")
println("Source: ${event.source}")
println("Time: ${event.timestamp}")

// React to specific changes
if ("feature.maintenance-mode" in event.changedKeys) {
enableMaintenanceMode()
}
}

// Stop watching
provider.stopWatching()

Sentinel Key (Azure)

Azure supports sentinel keys for efficient change detection:

val config = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
sentinelKey = "settings:sentinel", // Only poll this key
refreshInterval = 30.seconds
)

// When you update configuration in Azure:
// 1. Update your config values
// 2. Update the sentinel key value
// 3. Client detects sentinel change and refreshes all config

Health Checks

Monitor provider health for observability:

val health = provider.healthCheck()
if (health.isOk) {
val status = health.value
println("Healthy: ${status.isHealthy}")
println("Latency: ${status.latencyMs}ms")
println("Keys: ${status.configKeyCount}")
println("Last refresh: ${status.lastSuccessfulRefresh}")
} else {
println("Health check failed: ${health.error.message}")
}

// Integration with health endpoint
@Inject
class HealthController(
private val configProvider: CloudConfigProvider
) {
suspend fun checkHealth(): HealthResponse {
val configHealth = configProvider.healthCheck()
return HealthResponse(
config = if (configHealth.isOk) "UP" else "DOWN",
configLatency = configHealth.getOrNull()?.latencyMs
)
}
}

Offline Fallback

Cloud providers can use offline cache for network failure resilience.

Configuration

val provider = RestConfigProviderImpl(
config = RestConfigProviderConfig(
baseUrl = "https://config.mycompany.com",
tenantId = "my-tenant",
offlineCacheEnabled = true
),
offlineCache = KmpSettingsStorage()
)

Fallback Behavior

Offline Cache Fallback Flow

Best Practices

1. Use DefaultAzureCredential in Azure

// Recommended - flexible credential chain
val config = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
credentialOptions = AzureCredentialOptions(
authMethod = AzureAuthMethod.DEFAULT_CREDENTIAL
)
)

2. Always Enable Offline Cache

// Resilient to network failures
val provider = RestConfigProviderImpl(
config = config.copy(offlineCacheEnabled = true),
offlineCache = KmpSettingsStorage()
)

3. Use Labels for Environments

val config = AzureAppConfigProviderConfig(
labelFilter = when (environment) {
"prod" -> "production"
"staging" -> "staging"
else -> "development"
}
)

4. Handle Refresh Failures Gracefully

suspend fun refreshWithRetry(maxRetries: Int = 3): RefreshResult? {
repeat(maxRetries) { attempt ->
val result = provider.refresh()
if (result.isOk) {
return result.value
}
log.warn("Refresh attempt ${attempt + 1} failed: ${result.error.message}")
delay(1000L * (attempt + 1)) // Exponential backoff
}
log.error("All refresh attempts failed, using cached config")
return null
}

5. Use Sentinel Keys for Efficient Polling

// Only polls sentinel key, not all configuration
val config = AzureAppConfigProviderConfig(
sentinelKey = "app:version",
refreshInterval = 30.seconds
)