kotlin-app-config
Sealed class configuration pattern for Kotlin applications with environment-specific settings
When & Why to Use This Skill
This Claude skill provides a robust, type-safe pattern for managing environment-specific configurations in Kotlin applications using sealed classes. It addresses the common challenge of configuration management by ensuring compile-time validation, clear environment separation (Local, Dev, Prod), and seamless integration with external configuration libraries. By adopting this pattern, developers can eliminate runtime configuration errors and improve the overall maintainability and testability of their codebase.
Use Cases
- Managing sensitive credentials and infrastructure endpoints (Database, Kafka, Azure AD) across distinct deployment environments like Local, Dev, and Production.
- Implementing type-safe configuration loading from environment variables or system properties to replace error-prone string-based lookups.
- Creating isolated and predictable configurations for unit and integration testing to ensure consistent test results.
- Standardizing configuration architecture across large-scale Kotlin microservices to improve code readability and self-documentation for engineering teams.
| name | kotlin-app-config |
|---|---|
| description | Sealed class configuration pattern for Kotlin applications with environment-specific settings |
Kotlin Application Configuration Skill
This skill provides patterns for type-safe environment configuration using Kotlin sealed classes.
Sealed Class Configuration Pattern
sealed class Environment(
val name: String,
val databaseUrl: String,
val kafkaBrokers: String,
val azureAdIssuer: String
) {
data object Local : Environment(
name = "local",
databaseUrl = "jdbc:postgresql://localhost:5432/myapp",
kafkaBrokers = "localhost:9092",
azureAdIssuer = "http://localhost:8080/azuread"
)
data class Dev(
private val env: Map<String, String>
) : Environment(
name = "dev",
databaseUrl = env.getValue("DATABASE_URL"),
kafkaBrokers = env.getValue("KAFKA_BROKERS"),
azureAdIssuer = env.getValue("AZURE_OPENID_CONFIG_ISSUER")
)
data class Prod(
private val env: Map<String, String>
) : Environment(
name = "prod",
databaseUrl = env.getValue("DATABASE_URL"),
kafkaBrokers = env.getValue("KAFKA_BROKERS"),
azureAdIssuer = env.getValue("AZURE_OPENID_CONFIG_ISSUER")
)
companion object {
fun from(env: Map<String, String>): Environment {
return when (env["Nais_CLUSTER_NAME"]) {
"dev-gcp" -> Dev(env)
"prod-gcp" -> Prod(env)
else -> Local
}
}
}
}
Using Configuration
fun main() {
val env = Environment.from(System.getenv())
val dataSource = createDataSource(env.databaseUrl)
val kafkaProducer = createKafkaProducer(env.kafkaBrokers)
logger.info("Starting application in ${env.name} environment")
}
With Konfig Library
import com.natpryce.konfig.*
data class AppConfig(
val database: DatabaseConfig,
val kafka: KafkaConfig,
val azure: AzureConfig
)
data class DatabaseConfig(
val url: String,
Alternative: Sealed Interface Pattern (navikt/hotlibs)
Production pattern from navikt/hotlibs supporting multiple cluster types:
sealed interface Environment {
val cluster: String
val tier: Tier
enum class Tier { TEST, LOCAL, DEV, PROD }
companion object {
private val all: List<Environment> = listOf(
TestEnvironment,
LocalEnvironment,
GcpEnvironment.DEV,
GcpEnvironment.PROD
)
val current: Environment by lazy {
val cluster = System.getenv("Nais_CLUSTER_NAME")
all.find { it.cluster == cluster } ?: LocalEnvironment
}
}
}
sealed class DefaultEnvironment(
override val cluster: String,
override val tier: Environment.Tier
) : Environment
object TestEnvironment : DefaultEnvironment("test", Environment.Tier.TEST)
object LocalEnvironment : DefaultEnvironment("local", Environment.Tier.LOCAL)
enum class GcpEnvironment(
override val cluster: String,
override val tier: Environment.Tier
) : Environment {
DEV("dev-gcp", Environment.Tier.DEV),
PROD("prod-gcp", Environment.Tier.PROD)
}
data class DatabaseConfig(
val url: String,
val username: String,
val password: String
)
data class KafkaConfig(
val brokers: String,
val topic: String
)
data class AzureConfig(
val clientId: String,
val issuer: String,
val jwksUri: String
)
fun loadConfig(): AppConfig {
val config = ConfigurationProperties.systemProperties() overriding
EnvironmentVariables()
return AppConfig(
database = DatabaseConfig(
url = config[Key("database.url", stringType)],
username = config[Key("database.username", stringType)],
password = config[Key("database.password", stringType)]
),
kafka = KafkaConfig(
brokers = config[Key("kafka.brokers", stringType)],
topic = config[Key("kafka.topic", stringType)]
),
azure = AzureConfig(
clientId = config[Key("azure.client.id", stringType)],
issuer = config[Key("azure.issuer", stringType)],
jwksUri = config[Key("azure.jwks.uri", stringType)]
)
)
}
Benefits
- Type Safety: Compile-time validation of configuration
- Environment Separation: Clear boundaries between local/dev/prod
- Testability: Easy to create test configurations
- Documentation: Configuration structure is self-documenting