Configuration
The IDK provides a powerful configuration system that lets you read configuration values from multiple sources without worrying about where they come from. Your application code simply calls configService.getProperty("my.key") and the system handles resolution from environment variables, property files, cloud providers, and databases.
Quick Start
The most common pattern is reading configuration values through the ConfigService:
- Kotlin
- iOS/Swift
// Get ConfigService from your DI component
val configService: ConfigService = appComponent.configService
// Read a string property
val apiUrl = configService.getProperty("api.base.url", String::class)
// Read with a default value
val timeout = configService.getProperty("http.timeout.ms", Int::class, 30000)
// Read a required property (throws if missing)
val apiKey = configService.getRequiredProperty("api.key", String::class)
// Check if a property exists
if (configService.containsProperty("feature.enabled")) {
// ...
}
// Get ConfigService from your DI component
let configService = appComponent.configService
// Read a string property
let apiUrl = configService.getProperty(key: "api.base.url", targetType: String.self)
// Read with a default value
let timeout = configService.getProperty(key: "http.timeout.ms", targetType: Int.self, defaultValue: 30000)
// Read a required property (throws if missing)
let apiKey = configService.getRequiredProperty(key: "api.key", targetType: String.self)
// Check if a property exists
if configService.containsProperty(key: "feature.enabled") {
// ...
}
ConfigService API
The ConfigService interface provides type-safe access to configuration:
| Method | Description |
|---|---|
getProperty(key, type) | Get a property value, returns null if not found |
getProperty(key, type, default) | Get a property with a default fallback |
getRequiredProperty(key, type) | Get a property, throws if not found |
containsProperty(key) | Check if a property exists |
getAllProperties() | Get all properties as a map |
getSubProperties(prefix) | Get all properties under a prefix |
getActiveProfile() | Get the current configuration profile |
getAppName() | Get the application name |
Supported Types
The configuration system supports automatic type conversion:
// String
val name = configService.getProperty("app.name", String::class)
// Numbers
val port = configService.getProperty("server.port", Int::class)
val timeout = configService.getProperty("timeout.ms", Long::class)
val ratio = configService.getProperty("cache.ratio", Double::class)
// Boolean
val enabled = configService.getProperty("feature.enabled", Boolean::class)
Configuration Sources
Configuration values are resolved from multiple sources in priority order:
| Priority | Source | Description |
|---|---|---|
| 1 (Highest) | Environment Variables | API_BASE_URL maps to api.base.url |
| 2 | System Properties | JVM -D properties |
| 3 | Cloud Providers | Azure App Config, REST API servers |
| 4 | Database | Persisted settings (VDX only) |
| 5 | Property Files | application-{profile}.properties |
| 6 (Lowest) | Programmatic Defaults | Code-defined defaults |
Higher priority sources override lower priority ones. This means environment variables always win, allowing production overrides without code changes.
Key Normalization
Keys are normalized for consistent lookup across sources:
| Property Key | Environment Variable | Normalized Form |
|---|---|---|
api.base.url | API_BASE_URL | api.base.url |
http.timeout-ms | HTTP_TIMEOUT_MS | http.timeout.ms |
OAuth2.ClientId | OAUTH2_CLIENTID | oauth2.clientid |
You can use any format when reading properties - they'll all resolve to the same value:
// All these read the same configuration value
configService.getProperty("api.base.url", String::class)
configService.getProperty("api.baseUrl", String::class)
configService.getProperty("api.base-url", String::class)
Property Files
Create application-{profile}.properties files in your classpath:
# API Configuration
api.base.url=https://api.production.example.com
api.timeout.ms=30000
# Feature Flags
feature.new-dashboard.enabled=true
feature.beta-features.enabled=false
# Logging
logging.level=INFO
# API Configuration
api.base.url=http://localhost:8080
api.timeout.ms=60000
# Feature Flags
feature.new-dashboard.enabled=true
feature.beta-features.enabled=true
# Logging
logging.level=DEBUG
The profile is set during application initialization:
- Kotlin
- iOS/Swift
val appComponent = IdkAppComponent.init(
appId = "my-app",
profile = "production", // Loads application-production.properties
version = "1.0.0"
)
let appComponent = IdkAppComponent.companion.doInit(
appId: "my-app",
profile: "production", // Loads application-production.properties
version: "1.0.0"
)
Environment Variables
Environment variables provide the highest priority configuration source. They're ideal for:
- Production deployments
- Secrets and credentials
- Container/Kubernetes configurations
- CI/CD pipeline overrides
# Set configuration via environment variables
export API_BASE_URL=https://api.example.com
export API_KEY=secret-key-value
export LOG_LEVEL=DEBUG
export FEATURE_NEW_DASHBOARD_ENABLED=true
Conversion Rules
| Property Format | Environment Variable |
|---|---|
api.base.url | API_BASE_URL |
oauth2.client-id | OAUTH2_CLIENT_ID |
kms.providers.aws.region | KMS_PROVIDERS_AWS_REGION |
Writing Configuration
You can write configuration programmatically using property sources:
- Kotlin
// Add a property source with configuration values
val myConfig = MapPropertySource(
name = "my-custom-config",
source = mapOf(
"api.base.url" to "https://api.example.com",
"http.timeout.ms" to 30000,
"feature.enabled" to true
)
)
configService.addPropertySource(myConfig)
For persistent configuration storage, see Database Persistence (EDK/VDX only).
Scoped Configuration
The configuration system supports hierarchical scopes for multi-tenant applications:
| Scope | Description | Use Cases |
|---|---|---|
| App | Application-wide settings | API endpoints, default timeouts, feature flags |
| Tenant | Organization-specific settings | API keys, branding, org feature flags |
| Principal | User-specific settings | Preferences, individual credentials |
Each scope inherits from its parent, so tenant settings can override app settings, and principal settings can override tenant settings.
- Kotlin
// App-level configuration (shared by all tenants)
val appConfigService: AppConfigService = appComponent.appConfigService
val apiUrl = appConfigService.getProperty("api.base.url", String::class)
// Tenant-level configuration (inherits from app)
val tenantConfigService: TenantConfigService = tenantComponent.tenantConfigService
val tenantApiKey = tenantConfigService.getProperty("api.subscription.key", String::class)
// Principal-level configuration (inherits from tenant)
val principalConfigService: PrincipalConfigService = principalComponent.principalConfigService
val userTimeout = principalConfigService.getProperty("http.timeout.ms", Int::class)
For more details on multi-tenant configuration, see Multi-Tenancy.
Common Configuration Patterns
Feature Flags
// Check if a feature is enabled
val isNewDashboardEnabled = configService.getProperty(
"feature.new-dashboard.enabled",
Boolean::class
) ?: false
if (isNewDashboardEnabled) {
showNewDashboard()
} else {
showLegacyDashboard()
}
API Client Configuration
// Configure an API client from properties
val apiConfig = ApiClientConfig(
baseUrl = configService.getRequiredProperty("api.base.url", String::class),
timeoutMs = configService.getProperty("api.timeout.ms", Long::class, 30000L),
retryCount = configService.getProperty("api.retry.count", Int::class, 3)
)
Getting All Properties with a Prefix
// Get all properties under "kms.providers"
val kmsProviderProps = configService.getSubProperties(
prefixes = setOf("kms.providers"),
stripPrefix = true // "kms.providers.aws.region" becomes "aws.region"
)
kmsProviderProps.forEach { (key, value) ->
println("$key = $value")
}
Best Practices
Use environment variables for secrets. Never commit API keys, passwords, or other sensitive values to source control. Use environment variables in production.
Define sensible defaults. Property files should contain reasonable defaults for development. Production values come from environment variables.
Use descriptive key names. Follow dot-notation conventions: component.feature.setting (e.g., http.client.timeout.ms).
Validate at startup. Check that required configuration is present before serving requests:
fun validateConfig(configService: ConfigService) {
val requiredKeys = listOf("api.base.url", "api.key", "database.url")
val missing = requiredKeys.filter { !configService.containsProperty(it) }
if (missing.isNotEmpty()) {
throw IllegalStateException("Missing required configuration: $missing")
}
}
Use typed configuration classes for complex configuration:
data class DatabaseConfig(
val url: String,
val username: String,
val password: String,
val poolSize: Int = 10
) {
companion object {
fun fromConfigService(config: ConfigService) = DatabaseConfig(
url = config.getRequiredProperty("database.url", String::class),
username = config.getRequiredProperty("database.username", String::class),
password = config.getRequiredProperty("database.password", String::class),
poolSize = config.getProperty("database.pool.size", Int::class, 10)
)
}
}
Next Steps
- Property Resolution - Deep dive into how properties are resolved
- Configuration Providers - Available configuration providers and how to enable them
- Secrets Management - Handling sensitive configuration values
- Multi-Tenancy - Scoped configuration for multi-tenant applications