This episode covers managing application configuration with Groovy using ConfigSlurper, secure secret handling, and external config sources. You will also learn per-environment behavior and runtime overrides for applications running in many environments.

Production applications need configuration that can change without changing code. Episode 14 covers how to manage configuration with Groovy elegantly, including storing secrets safely and making an application behave differently in each environment.
You'll learn ConfigSlurper for parsing configuration, secure handling of secrets, external config sources, as well as environment-specific behavior and runtime overrides.
Groovy provides ConfigSlurper — a parser specifically for Groovy-formatted configuration files. The configuration structure is written as nested maps:
// application.groovy
app {
name = "belajar-groovy"
port = 8080
retry = 3
}
database {
host = "db.internal"
timeout = 30
}Read and access that configuration:
def config = new ConfigSlurper().parse(new File("application.groovy").toURL())
println config.app.name
println config.database.hostnew ConfigSlurper().parse(...) parses the configuration file into an object accessible with dot notation. config.app.name reads a nested value without manual parsing — a pattern far more convenient than properties files.
ConfigSlurper has built-in support for environments. Values can be defined per environment and overridden according to context:
environments {
dev {
app {
name = "belajar-groovy-dev"
debug = true
}
}
production {
app {
name = "belajar-groovy"
debug = false
}
}
}Load it by specifying the environment:
def env = System.getProperty("env") ?: "dev"
def config = new ConfigSlurper(env).parse(new File("application.groovy").toURL())
println config.app.name
println "Debug: ${config.app.debug}"new ConfigSlurper(env).parse(...) loads the block for the matching environment. System.getProperty("env") ?: "dev" picks the environment from a system property with a fallback to dev — a flexible runtime override pattern.
Configuration can be overridden at runtime without changing files. Environment or system property values take precedence:
def env = System.getenv("ENV") ?: "dev"
def config = new ConfigSlurper(env).parse(new File("application.groovy").toURL())
config.app.port = System.getProperty("app.port")?.toInteger() ?: config.app.port
println "Port: ${config.app.port}"config.app.port = System.getProperty("app.port") ?: config.app.port overrides the value from a system property when available. ?.toInteger() ?: config.app.port combines safe navigation and Elvis for a clean fallback.
The first rule of security: secrets are never written in code or in configuration files that enter version control. Here are the recommended practices:
The simplest pattern: secrets come from environment variables:
def apiKey = System.getenv("API_KEY")
if (!apiKey) {
throw new IllegalStateException("API_KEY belum diset")
}
println "API key tersedia (disembunyikan)"System.getenv("API_KEY") reads the secret from the environment, and if (!apiKey) throw new IllegalStateException(...) stops the application when the secret is missing. throw new IllegalStateException("API_KEY belum diset") prevents the application from running with incomplete configuration.
For more complex secrets, use a secret manager such as HashiCorp Vault or OpenBao. Groovy can read secrets from their APIs:
def uri = "https://vault.internal/v1/kv/data/app"
def token = System.getenv("VAULT_TOKEN")
def conn = new URL(uri).openConnection()
conn.setRequestProperty("X-Vault-Token", token)
def response = conn.inputStream.text
println "Response diterima (ukuran ${response.length()} byte)"new URL(uri).openConnection() opens an HTTP connection to Vault, and conn.setRequestProperty("X-Vault-Token", token) sends the authentication token. conn.inputStream.text reads the response. Secret managers will be covered in more depth in episode 20.
Let's combine the patterns we've learned:
def env = System.getenv("ENV") ?: "dev"
def config = new ConfigSlurper(env).parse(new File("application.groovy").toURL())
config.database.password = System.getenv("DB_PASSWORD") ?: config.database.password
config.app.port = System.getProperty("app.port")?.toInteger() ?: config.app.port
println "App: ${config.app.name} di port ${config.app.port}"
println "DB: ${config.database.host}"config.database.password = System.getenv("DB_PASSWORD") ?: config.database.password merges secrets from the environment with default values from the file. config.app.port = System.getProperty("app.port")?.toInteger() ?: config.app.port applies the runtime override. The result: one configuration file serving many environments safely.
Episode 14 equipped you with modern configuration management: ConfigSlurper for parsing, environment blocks for environment differences, runtime overrides, and secure secret storage patterns.
The key takeaways:
environments {} enables different configuration per environment.In episode 15 next, we'll discuss metaprogramming and AST transformations — dynamic methods, propertyMissing, methodMissing, comparing runtime metaprogramming with compile-time AST transformations, and examples of decorator and dynamic proxy patterns.