Working with Groovy property files is simple. See 10 tested examples to read, write, merge properties, use defaults, and convert between formats on Groovy 5.x.
“Configuration is the soul of every application. Get it wrong and nothing works. Get it right and nobody notices.”
Dave Thomas, The Pragmatic Programmer
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 14 minutes
Every application needs configuration. Database URLs, API keys, feature flags, environment-specific settings – they all need to live somewhere outside your code. In the Java world, the humble .properties file has been the go-to format for decades. And when you work with Groovy property files, you get all the power of Java’s Properties class plus Groovy’s concise syntax and helpful extensions. For DSL-based configuration, Groovy also offers ConfigSlurper as a more expressive alternative.
In this guide, we’ll walk through 10 practical, tested examples covering everything from reading and writing properties to merging files, handling defaults, variable substitution, and even comparing Properties with Groovy’s ConfigSlurper. These patterns work for standalone scripts and full applications alike, helping you manage configuration cleanly and reliably.
If you’re new to file I/O in Groovy, start with our Groovy File Read, Write, Delete guide first. For more advanced configuration management, check out Groovy ConfigSlurper which offers a DSL-based alternative to property files.
Table of Contents
What Are Property Files?
A .properties file is a simple key-value text format that Java (and Groovy) can read natively using the java.util.Properties class. Each line contains a key, an equals sign (or colon), and a value. Comments start with # or !.
Sample .properties File
# Application Configuration app.name=MyGroovyApp app.version=2.1.0 # Database Settings db.host=localhost db.port=5432 db.name=mydb db.user=admin db.password=secret123 # Feature Flags feature.cache.enabled=true feature.debug.mode=false
The format has been around since the earliest days of Java and remains widely used because of its simplicity. Groovy makes working with these files even easier thanks to its concise file I/O, closure-based iteration, and the ability to treat Properties objects like Groovy maps.
When to Choose Property Files
With so many configuration formats available – YAML, JSON, TOML, ConfigSlurper DSL – you might wonder when plain .properties files are still the right choice. The answer comes down to simplicity, compatibility, and tooling support.
Property files are the best fit when:
- Your configuration is flat key-value pairs – no nesting, no complex data structures, just simple settings like database URLs, feature flags, and application metadata.
- You need cross-language or cross-tool compatibility – virtually every language and framework can read
.propertiesfiles. Spring Boot, Gradle, Maven, Ant, and most Java libraries support them natively. - Non-developers need to edit configuration – the format is so simple that anyone can understand and modify it without risk of syntax errors like mismatched braces or incorrect indentation.
- You want environment-specific overrides – loading a base file and overlaying environment-specific values (dev, staging, production) is a well-established pattern with
.propertiesfiles.
On the other hand, if your configuration requires hierarchical structure, typed values, or conditional logic, consider ConfigSlurper or a format like YAML. For most scripts, CLI tools, and simple applications, property files remain the simplest and most portable option.
10 Practical Examples
Let’s get hands-on. Every example below has been tested on Groovy 5.x with Java 17+. We’ll start with the basics and work our way up to more advanced patterns.
Example 1: Reading a .properties File
What we’re doing: Loading a .properties file from disk and accessing its values by key. This is the most fundamental operation you’ll perform with property files.
Example 1: Reading Properties
// First, create a sample properties file for our examples
def sampleContent = """\
app.name=MyGroovyApp
app.version=2.1.0
db.host=localhost
db.port=5432
db.name=mydb
db.user=admin
feature.cache.enabled=true
feature.debug.mode=false
""".stripIndent()
new File('app.properties').text = sampleContent
// --- Reading the properties file ---
def props = new Properties()
new File('app.properties').withInputStream { stream ->
props.load(stream)
}
// Access individual properties
println "App Name: ${props.getProperty('app.name')}"
println "DB Host: ${props.getProperty('db.host')}"
println "DB Port: ${props.getProperty('db.port')}"
println "Cache Enabled: ${props.getProperty('feature.cache.enabled')}"
// Groovy shorthand - use dot notation or bracket access
println "Version: ${props['app.version']}"
// Total properties loaded
println "Total keys loaded: ${props.size()}"
Output
App Name: MyGroovyApp DB Host: localhost DB Port: 5432 Cache Enabled: true Version: 2.1.0 Total keys loaded: 8
What happened here: We created a Properties object and loaded the file using withInputStream, which automatically closes the stream when done. The getProperty() method returns the value for a given key, and Groovy also lets you use bracket notation (props['key']) for a cleaner look. Every value comes back as a String – if you need an integer or boolean, you’ll need to convert it yourself.
Example 2: Writing Properties to File
What we’re doing: Creating a Properties object in code, populating it with key-value pairs, and saving it to a .properties file on disk.
Example 2: Writing Properties
def props = new Properties()
// Set properties using setProperty()
props.setProperty('server.host', '0.0.0.0')
props.setProperty('server.port', '8080')
props.setProperty('server.context', '/api')
// Groovy shorthand - bracket notation
props['logging.level'] = 'INFO'
props['logging.file'] = '/var/log/app.log'
props['logging.maxSize'] = '10MB'
// Save to file with a comment header
new File('server.properties').withOutputStream { stream ->
props.store(stream, 'Server Configuration - Generated by Groovy')
}
// Verify by reading it back
println "Written file contents:"
println new File('server.properties').text
// Also verify programmatically
def verify = new Properties()
new File('server.properties').withInputStream { verify.load(it) }
println "Verified server.port = ${verify['server.port']}"
Output
Written file contents: #Server Configuration - Generated by Groovy #Mon Mar 10 12:00:00 UTC 2026 logging.file=/var/log/app.log logging.level=INFO logging.maxSize=10MB server.context=/api server.host=0.0.0.0 server.port=8080 Verified server.port = 8080
What happened here: The store() method writes the properties to a file with an optional comment header. Java’s Properties class automatically adds a timestamp comment. Note that the keys are written in alphabetical order – Properties does not preserve insertion order. If ordering matters, consider using a LinkedHashMap and writing the file manually. See our Groovy Map Tutorial for more on map types.
Example 3: Setting Default Values for Missing Keys
What we’re doing: Providing fallback values when a property key doesn’t exist in the file. This is essential for building resilient applications that don’t crash on missing configuration.
Example 3: Default Values
// Load the properties file from Example 1
def props = new Properties()
new File('app.properties').withInputStream { props.load(it) }
// getProperty() with a default value (second argument)
def host = props.getProperty('db.host', 'localhost')
def port = props.getProperty('db.port', '3306')
def timeout = props.getProperty('db.timeout', '30000') // key doesn't exist
def poolSize = props.getProperty('db.pool.size', '10') // key doesn't exist
println "Host: ${host}" // from file
println "Port: ${port}" // from file
println "Timeout: ${timeout}" // default used
println "Pool Size: ${poolSize}" // default used
// Using Groovy's elvis operator for even cleaner defaults
def maxRetries = props['connection.retries'] ?: '3'
def sslEnabled = props['ssl.enabled'] ?: 'false'
println "Max Retries: ${maxRetries}"
println "SSL Enabled: ${sslEnabled}"
// Build a complete config map with defaults
def defaults = [
'db.host' : 'localhost',
'db.port' : '3306',
'db.timeout' : '30000',
'db.pool.size': '10',
'db.ssl' : 'false'
]
def config = defaults.collectEntries { key, defaultVal ->
[key, props.getProperty(key, defaultVal)]
}
println "\nResolved config with defaults:"
config.each { k, v -> println " ${k} = ${v}" }
Output
Host: localhost Port: 5432 Timeout: 30000 Pool Size: 10 Max Retries: 3 SSL Enabled: false Resolved config with defaults: db.host = localhost db.port = 5432 db.timeout = 30000 db.pool.size = 10 db.ssl = false
What happened here: The two-argument form of getProperty(key, default) returns the default when the key is missing. Groovy’s elvis operator (?:) works too, but be careful – it treats empty strings as falsy, while getProperty() only falls back when the key is truly absent. The defaults map pattern at the end is a clean way to ensure every required key has a value. For more on the elvis operator, see Groovy Elvis Operator.
Example 4: Iterating Over All Properties
What we’re doing: Looping through every key-value pair in a properties file. Useful for debugging, logging configuration at startup, or transforming properties into other formats.
Example 4: Iterating Properties
def props = new Properties()
new File('app.properties').withInputStream { props.load(it) }
// Method 1: Groovy each() closure
println "=== All Properties (each) ==="
props.each { key, value ->
println " ${key} = ${value}"
}
// Method 2: Filter properties by prefix
println "\n=== Database Properties Only ==="
props.findAll { key, value ->
key.startsWith('db.')
}.each { key, value ->
println " ${key} = ${value}"
}
// Method 3: Sorted output
println "\n=== Sorted Alphabetically ==="
props.sort().each { key, value ->
println " ${key} = ${value}"
}
// Method 4: Group by prefix
println "\n=== Grouped by Category ==="
def grouped = props.groupBy { key, value ->
key.contains('.') ? key.split('\\.')[0] : 'other'
}
grouped.each { category, entries ->
println " [${category}]"
entries.each { key, value ->
println " ${key} = ${value}"
}
}
// Method 5: Get just the keys or values
println "\nAll keys: ${props.stringPropertyNames().sort()}"
Output
=== All Properties (each) ===
app.name = MyGroovyApp
app.version = 2.1.0
db.host = localhost
db.port = 5432
db.name = mydb
db.user = admin
feature.cache.enabled = true
feature.debug.mode = false
=== Database Properties Only ===
db.host = localhost
db.port = 5432
db.name = mydb
db.user = admin
=== Sorted Alphabetically ===
app.name = MyGroovyApp
app.version = 2.1.0
db.host = localhost
db.name = mydb
db.port = 5432
db.user = admin
feature.cache.enabled = true
feature.debug.mode = false
=== Grouped by Category ===
[app]
app.name = MyGroovyApp
app.version = 2.1.0
[db]
db.host = localhost
db.port = 5432
db.name = mydb
db.user = admin
[feature]
feature.cache.enabled = true
feature.debug.mode = false
All keys: [app.name, app.version, db.host, db.name, db.port, db.user, feature.cache.enabled, feature.debug.mode]
What happened here: Because Groovy’s Properties object implements Map, you can use all the familiar Groovy collection methods – each(), findAll(), sort(), groupBy(), and more. The findAll with a prefix filter is a pattern you’ll use constantly when your properties are grouped by dotted namespaces. For a deeper look at these collection methods, see Groovy each Loop.
Example 5: System Properties Access
What we’re doing: Reading and modifying JVM system properties. These are built-in properties that provide information about the runtime environment, and you can also set custom ones for application configuration.
Example 5: System Properties
// Reading common system properties
println "=== JVM System Properties ==="
println "Java Version: ${System.getProperty('java.version')}"
println "Java Home: ${System.getProperty('java.home')}"
println "OS Name: ${System.getProperty('os.name')}"
println "OS Arch: ${System.getProperty('os.arch')}"
println "User Name: ${System.getProperty('user.name')}"
println "User Home: ${System.getProperty('user.home')}"
println "User Dir: ${System.getProperty('user.dir')}"
println "File Separator: ${System.getProperty('file.separator')}"
println "Line Separator: ${System.getProperty('line.separator').bytes}"
// Setting custom system properties
System.setProperty('myapp.env', 'development')
System.setProperty('myapp.debug', 'true')
System.setProperty('myapp.logdir', '/tmp/logs')
println "\n=== Custom System Properties ==="
println "Environment: ${System.getProperty('myapp.env')}"
println "Debug Mode: ${System.getProperty('myapp.debug')}"
println "Log Dir: ${System.getProperty('myapp.logdir')}"
// System properties with defaults
def maxMemory = System.getProperty('myapp.maxMemory', '512m')
println "Max Memory: ${maxMemory}" // uses default
// Get all system properties (filtered)
println "\n=== All 'myapp' System Properties ==="
System.properties.findAll { key, value ->
key.toString().startsWith('myapp.')
}.each { key, value ->
println " ${key} = ${value}"
}
// Common pattern: use -D flags from command line
// Run with: groovy -Dmyapp.env=production script.groovy
def env = System.getProperty('myapp.env', 'development')
println "\nActive environment: ${env}"
Output
=== JVM System Properties === Java Version: 17.0.9 Java Home: /usr/lib/jvm/java-17-openjdk OS Name: Linux OS Arch: amd64 User Name: developer User Home: /home/developer User Dir: /home/developer/projects File Separator: / Line Separator: [10] === Custom System Properties === Environment: development Debug Mode: true Log Dir: /tmp/logs Max Memory: 512m === All 'myapp' System Properties === myapp.debug = true myapp.env = development myapp.logdir = /tmp/logs Active environment: development
What happened here: System properties are JVM-wide and accessible from anywhere in your application. They’re commonly set via -D flags on the command line (groovy -Dmyapp.env=production script.groovy) or programmatically with System.setProperty(). The System.properties object is a Properties instance, so all the Groovy collection methods work on it. This is a practical way to pass configuration without files – useful in Docker containers and CI/CD pipelines.
Example 6: Merging Multiple Property Files
What we’re doing: Loading properties from multiple files and merging them together, with later files overriding earlier ones. This is the foundation of environment-specific configuration.
Example 6: Merging Properties
// Create base configuration
new File('base.properties').text = """\
app.name=MyGroovyApp
db.host=localhost
db.port=5432
db.pool.size=5
logging.level=INFO
cache.ttl=3600
""".stripIndent()
// Create override configuration (e.g., production overrides)
new File('override.properties').text = """\
db.host=prod-db.example.com
db.port=5433
db.pool.size=20
logging.level=WARN
""".stripIndent()
// Method 1: Sequential loading (later values override)
def merged = new Properties()
// Load base first
new File('base.properties').withInputStream { merged.load(it) }
println "After base load: db.host = ${merged['db.host']}"
// Load overrides - matching keys are replaced
new File('override.properties').withInputStream { merged.load(it) }
println "After override: db.host = ${merged['db.host']}"
println "\n=== Merged Configuration ==="
merged.sort().each { k, v -> println " ${k} = ${v}" }
// Method 2: Utility function for merging multiple files
def mergePropertyFiles(List<String> filePaths) {
def result = new Properties()
filePaths.each { path ->
def file = new File(path)
if (file.exists()) {
file.withInputStream { result.load(it) }
println " Loaded: ${path} (${file.readLines().findAll { !it.startsWith('#') && it.trim() }.size()} entries)"
} else {
println " Skipped (not found): ${path}"
}
}
return result
}
println "\n=== Merge with utility ==="
def config = mergePropertyFiles([
'base.properties',
'override.properties',
'local.properties' // doesn't exist - safely skipped
])
println "\nFinal config:"
config.sort().each { k, v -> println " ${k} = ${v}" }
Output
After base load: db.host = localhost After override: db.host = prod-db.example.com === Merged Configuration === app.name = MyGroovyApp cache.ttl = 3600 db.host = prod-db.example.com db.port = 5433 db.pool.size = 20 logging.level = WARN === Merge with utility === Loaded: base.properties (6 entries) Loaded: override.properties (4 entries) Skipped (not found): local.properties Final config: app.name = MyGroovyApp cache.ttl = 3600 db.host = prod-db.example.com db.port = 5433 db.pool.size = 20 logging.level = WARN
What happened here: Calling load() multiple times on the same Properties object merges the entries. If a key exists in both files, the later file wins. This is the standard pattern for layered configuration: load defaults first, then environment overrides, then local developer overrides. The utility function adds safety by skipping missing files – useful when optional config files may or may not exist. For file existence checking patterns, see Groovy File Operations.
Example 7: Properties with Variable Substitution
What we’re doing: Implementing variable substitution so property values can reference other properties. Java’s Properties class doesn’t support this natively, but it’s easy to build with Groovy.
Example 7: Variable Substitution
// Create a properties file with placeholders
new File('template.properties').text = """\
app.name=MyGroovyApp
app.env=production
db.host=db-\${app.env}.example.com
db.name=\${app.name}_db
db.url=jdbc:postgresql://\${db.host}:5432/\${db.name}
log.dir=/var/log/\${app.name}/\${app.env}
log.file=\${log.dir}/app.log
""".stripIndent()
// Load raw properties
def raw = new Properties()
new File('template.properties').withInputStream { raw.load(it) }
println "=== Raw (before substitution) ==="
raw.sort().each { k, v -> println " ${k} = ${v}" }
// Resolve placeholders: replace ${key} with its value
def resolveProperties(Properties props) {
def resolved = new Properties()
resolved.putAll(props)
// Multiple passes to handle chained references
3.times {
resolved.each { key, value ->
def newValue = value.toString().replaceAll(/\$\{([^}]+)\}/) { match, refKey ->
resolved.getProperty(refKey, match[0])
}
resolved.setProperty(key.toString(), newValue)
}
}
return resolved
}
def config = resolveProperties(raw)
println "\n=== Resolved (after substitution) ==="
config.sort().each { k, v -> println " ${k} = ${v}" }
// Verify specific resolved values
println "\nDB URL: ${config['db.url']}"
println "Log File: ${config['log.file']}"
Output
=== Raw (before substitution) ===
app.env = production
app.name = MyGroovyApp
db.host = db-${app.env}.example.com
db.name = ${app.name}_db
db.url = jdbc:postgresql://${db.host}:5432/${db.name}
log.dir = /var/log/${app.name}/${app.env}
log.file = ${log.dir}/app.log
=== Resolved (after substitution) ===
app.env = production
app.name = MyGroovyApp
db.host = db-production.example.com
db.name = MyGroovyApp_db
db.url = jdbc:postgresql://db-production.example.com:5432/MyGroovyApp_db
log.dir = /var/log/MyGroovyApp/production
log.file = /var/log/MyGroovyApp/production/app.log
DB URL: jdbc:postgresql://db-production.example.com:5432/MyGroovyApp_db
Log File: /var/log/MyGroovyApp/production/app.log
What happened here: We wrote a resolveProperties() function that scans each value for ${key} placeholders and replaces them with the corresponding property value. We run three passes to handle chained references (where one placeholder resolves to another placeholder). This pattern is similar to what Spring Framework does with its PropertyPlaceholderConfigurer. For more on Groovy’s regex capabilities used here, see Groovy Regular Expressions.
Example 8: Converting Properties to/from Map
What we’re doing: Converting between Properties objects and Groovy maps. This is useful when you want to manipulate configuration with Groovy’s rich map API, or when you need to serialize config to JSON.
Example 8: Properties and Maps
// --- Properties to Map ---
def props = new Properties()
new File('app.properties').withInputStream { props.load(it) }
// Simple conversion: Properties implements Map, so this works
Map<String, String> flatMap = props.collectEntries { k, v -> [k.toString(), v.toString()] }
println "=== Flat Map ==="
flatMap.sort().each { k, v -> println " ${k} = ${v}" }
// Convert to nested map (dot-separated keys become nested structure)
def toNestedMap(Properties props) {
def result = [:]
props.each { key, value ->
def parts = key.toString().split('\\.')
def current = result
parts.eachWithIndex { part, idx ->
if (idx == parts.size() - 1) {
current[part] = value.toString()
} else {
current = current.computeIfAbsent(part) { [:] }
}
}
}
return result
}
def nested = toNestedMap(props)
println "\n=== Nested Map ==="
println " app.name: ${nested.app.name}"
println " db.host: ${nested.db.host}"
println " feature.cache.enabled: ${nested.feature.cache.enabled}"
// --- Map to Properties ---
def serverConfig = [
'server.host' : 'localhost',
'server.port' : '9090',
'server.threads' : '200',
'server.timeout' : '30000'
]
def serverProps = new Properties()
serverProps.putAll(serverConfig)
// Save the map-derived properties
new File('from-map.properties').withOutputStream { stream ->
serverProps.store(stream, 'Converted from Map')
}
println "\n=== Map to Properties File ==="
println new File('from-map.properties').text
// --- Round-trip: Map -> Properties -> JSON (via Groovy) ---
def jsonOutput = groovy.json.JsonOutput.prettyPrint(
groovy.json.JsonOutput.toJson(nested)
)
println "=== Nested Config as JSON ==="
println jsonOutput
Output
=== Flat Map ===
app.name = MyGroovyApp
app.version = 2.1.0
db.host = localhost
db.name = mydb
db.port = 5432
db.user = admin
feature.cache.enabled = true
feature.debug.mode = false
=== Nested Map ===
app.name: MyGroovyApp
db.host: localhost
feature.cache.enabled: true
=== Map to Properties File ===
#Converted from Map
#Mon Mar 10 12:00:00 UTC 2026
server.host=localhost
server.port=9090
server.threads=200
server.timeout=30000
=== Nested Config as JSON ===
{
"app": {
"name": "MyGroovyApp",
"version": "2.1.0"
},
"db": {
"host": "localhost",
"port": "5432",
"name": "mydb",
"user": "admin"
},
"feature": {
"cache": {
"enabled": "true"
},
"debug": {
"mode": "false"
}
}
}
What happened here: The flat conversion is simple since Properties already behaves like a map. The nested conversion splits dotted keys (like db.host) into a tree structure, which is handy for JSON serialization or when you want to access config with dot notation. The reverse – map to properties – is just putAll(). For more on JSON output, see our Groovy JSON Parsing guide.
Example 9: Properties vs ConfigSlurper Comparison
What we’re doing: Comparing Java’s Properties with Groovy’s ConfigSlurper – two different approaches to configuration management. This helps you choose the right tool for your project.
Example 9: Properties vs ConfigSlurper
// === Approach 1: Traditional Properties ===
new File('traditional.properties').text = """\
app.name=MyApp
db.host=localhost
db.port=5432
db.credentials.user=admin
db.credentials.password=secret
feature.flags.darkMode=true
feature.flags.betaAccess=false
""".stripIndent()
def props = new Properties()
new File('traditional.properties').withInputStream { props.load(it) }
// Accessing is flat - always string keys and string values
println "=== Properties Approach ==="
println "App: ${props['app.name']}"
println "DB Host: ${props['db.host']}"
println "DB Port: ${props['db.port'].toInteger()}" // manual conversion
println "Dark Mode: ${props['feature.flags.darkMode'].toBoolean()}" // manual conversion
// === Approach 2: ConfigSlurper ===
// ConfigSlurper uses a Groovy DSL - much more expressive
new File('config.groovy').text = """\
app {
name = 'MyApp'
}
db {
host = 'localhost'
port = 5432 // native int, not string
credentials {
user = 'admin'
password = 'secret'
}
}
feature {
flags {
darkMode = true // native boolean
betaAccess = false
}
}
environments {
development {
db.host = 'localhost'
db.port = 5432
}
production {
db.host = 'prod-db.example.com'
db.port = 5433
}
}
""".stripIndent()
def config = new ConfigSlurper('development').parse(new File('config.groovy').toURI().toURL())
println "\n=== ConfigSlurper Approach ==="
println "App: ${config.app.name}"
println "DB Host: ${config.db.host}" // native type
println "DB Port: ${config.db.port}" // already an int
println "Dark Mode: ${config.feature.flags.darkMode}" // already a boolean
println "DB User: ${config.db.credentials.user}"
// ConfigSlurper supports environments natively
def prodConfig = new ConfigSlurper('production').parse(new File('config.groovy').toURI().toURL())
println "\nProduction DB: ${prodConfig.db.host}:${prodConfig.db.port}"
// === Comparison Table ===
println "\n=== When to Use Which ==="
println """
| Feature | Properties | ConfigSlurper |
|---------------------|--------------------|----------------------|
| Format | .properties (flat) | .groovy (DSL) |
| Value types | Strings only | Any Groovy type |
| Nesting | Dot convention | Native nesting |
| Environments | Manual merge | Built-in support |
| Java interop | Native | Groovy only |
| IDE support | Excellent | Good |
| Security | Safe (no code) | Executes Groovy code |
| Human readability | Good | Very good |
""".stripIndent()
Output
=== Properties Approach === App: MyApp DB Host: localhost DB Port: 5432 Dark Mode: true === ConfigSlurper Approach === App: MyApp DB Host: localhost DB Port: 5432 Dark Mode: true DB User: admin Production DB: prod-db.example.com:5433 === When to Use Which === | Feature | Properties | ConfigSlurper | |---------------------|--------------------|----------------------| | Format | .properties (flat) | .groovy (DSL) | | Value types | Strings only | Any Groovy type | | Nesting | Dot convention | Native nesting | | Environments | Manual merge | Built-in support | | Java interop | Native | Groovy only | | IDE support | Excellent | Good | | Security | Safe (no code) | Executes Groovy code | | Human readability | Good | Very good |
What happened here: Both approaches work, but they excel in different scenarios. Use Properties when you need Java compatibility, when your config is simple key-value pairs, or when security matters (property files can’t execute code). Use ConfigSlurper when you want native types, nested structures, and built-in environment support. Many Groovy projects use both – .properties for simple settings and ConfigSlurper for complex configuration hierarchies. For the full details, see our Groovy ConfigSlurper guide.
Example 10: Environment-Specific Property Loading
What we’re doing: Building a complete environment-aware configuration loader that reads a base file, then overlays environment-specific properties based on a runtime flag. This is the pattern used by most real-world applications.
Example 10: Environment-Specific Loading
// Create environment-specific property files
new File('application.properties').text = """\
# Base / default configuration
app.name=MyGroovyApp
app.version=2.1.0
server.port=8080
db.host=localhost
db.port=5432
db.name=myapp
db.pool.size=5
logging.level=DEBUG
cache.enabled=true
cache.ttl=300
""".stripIndent()
new File('application-dev.properties').text = """\
# Development overrides
db.host=localhost
db.name=myapp_dev
logging.level=DEBUG
cache.enabled=false
""".stripIndent()
new File('application-prod.properties').text = """\
# Production overrides
server.port=80
db.host=prod-db.example.com
db.port=5433
db.name=myapp_prod
db.pool.size=50
logging.level=WARN
cache.ttl=3600
""".stripIndent()
// Environment-aware config loader
class AppConfig {
private Properties props = new Properties()
private String environment
AppConfig(String env = null) {
this.environment = env ?: System.getProperty('app.env', 'dev')
loadConfig()
}
private void loadConfig() {
// 1. Load base config
loadFile('application.properties')
// 2. Load environment-specific overrides
def envFile = "application-${environment}.properties"
loadFile(envFile)
// 3. System properties override everything (for -D flags)
System.properties.findAll { k, v ->
k.toString().startsWith('app.') ||
k.toString().startsWith('db.') ||
k.toString().startsWith('server.')
}.each { k, v ->
props.setProperty(k.toString(), v.toString())
}
println "Config loaded for environment: ${environment}"
println " Base: application.properties"
println " Env: ${envFile} ${new File(envFile).exists() ? '(found)' : '(not found)'}"
}
private void loadFile(String filename) {
def file = new File(filename)
if (file.exists()) {
file.withInputStream { props.load(it) }
}
}
String get(String key, String defaultValue = null) {
return props.getProperty(key, defaultValue)
}
int getInt(String key, int defaultValue = 0) {
def val = props.getProperty(key)
return val ? val.toInteger() : defaultValue
}
boolean getBool(String key, boolean defaultValue = false) {
def val = props.getProperty(key)
return val ? val.toBoolean() : defaultValue
}
void dump() {
println "\n=== Active Configuration (${environment}) ==="
props.sort().each { k, v -> println " ${k} = ${v}" }
}
}
// Usage: Development environment
println "--- Loading DEV config ---"
def devConfig = new AppConfig('dev')
devConfig.dump()
println "\nDev DB: ${devConfig.get('db.host')}:${devConfig.getInt('db.port')}"
println "Cache enabled: ${devConfig.getBool('cache.enabled')}"
// Usage: Production environment
println "\n--- Loading PROD config ---"
def prodConfig = new AppConfig('prod')
prodConfig.dump()
println "\nProd DB: ${prodConfig.get('db.host')}:${prodConfig.getInt('db.port')}"
println "Pool size: ${prodConfig.getInt('db.pool.size')}"
Output
--- Loading DEV config --- Config loaded for environment: dev Base: application.properties Env: application-dev.properties (found) === Active Configuration (dev) === app.name = MyGroovyApp app.version = 2.1.0 cache.enabled = false cache.ttl = 300 db.host = localhost db.name = myapp_dev db.pool.size = 5 db.port = 5432 logging.level = DEBUG server.port = 8080 Dev DB: localhost:5432 Cache enabled: false --- Loading PROD config --- Config loaded for environment: prod Base: application.properties Env: application-prod.properties (found) === Active Configuration (prod) === app.name = MyGroovyApp app.version = 2.1.0 cache.enabled = true cache.ttl = 3600 db.host = prod-db.example.com db.name = myapp_prod db.pool.size = 50 db.port = 5433 logging.level = WARN server.port = 80 Prod DB: prod-db.example.com:5433 Pool size: 50
What happened here: The AppConfig class implements a three-layer configuration strategy: base properties first, then environment-specific overrides, then system property overrides (from -D flags). This is the same pattern used by Spring Boot’s application.properties / application-{profile}.properties convention. The typed getter methods (getInt, getBool) handle the string-to-type conversion so calling code doesn’t have to. This class is a solid starting point for any Groovy application that needs environment-aware configuration.
Best Practices
Always Close Streams
Use Groovy’s withInputStream and withOutputStream instead of manually opening and closing streams. This guarantees the stream is closed even if an exception is thrown.
Good vs Bad Practice
// BAD: manual stream management
def stream = new FileInputStream('app.properties')
props.load(stream)
stream.close() // what if load() throws?
// GOOD: Groovy's withInputStream handles closing
new File('app.properties').withInputStream { props.load(it) }
Never Store Secrets in Property Files
Property files are plain text. Never commit passwords, API keys, or tokens to version control via property files. Use environment variables or a secrets manager instead.
// Instead of: db.password=my-secret-password
// Use environment variables:
def dbPassword = System.getenv('DB_PASSWORD') ?: props.getProperty('db.password', '')
Use Consistent Naming Conventions
Stick to a dot-separated, lowercase naming convention for keys. This makes properties predictable, easy to filter by prefix, and compatible with tools like Spring Boot that auto-map properties to objects.
# GOOD: consistent dot-separated lowercase app.name=MyApp db.connection.pool.size=10 feature.dark-mode.enabled=true # BAD: inconsistent naming AppName=MyApp DB_POOL_SIZE=10 enableDarkMode=true
Validate Configuration Early
Check for required properties at application startup rather than failing later at the point of use. A simple validation loop catches misconfiguration immediately.
def required = ['db.host', 'db.port', 'db.name']
def missing = required.findAll { !props.containsKey(it) }
if (missing) {
throw new IllegalStateException("Missing required config keys: ${missing}")
}
Document Your Properties
Add comments above each section in your property files. Future you (and your teammates) will appreciate knowing what each key controls and what values are acceptable.
# === Database Configuration === # Host: hostname or IP of the database server db.host=localhost # Port: default PostgreSQL port is 5432 db.port=5432 # Pool size: number of connections (5-50 recommended) db.pool.size=10
Conclusion
Groovy property files combine the universality of Java’s Properties class with Groovy’s concise, expressive syntax. The patterns in this guide cover the common scenarios you’ll encounter – from reading a simple config file to merging environment-specific overrides and building a complete configuration management system.
For simple key-value configuration that needs to work with Java code and standard tools, .properties files remain the best choice. When your configuration needs grow – native types, nested structures, environment blocks – consider upgrading to ConfigSlurper. Many projects use both, and now you know how to work with each.
Summary
- Use
withInputStreamfor safe file loading – Groovy handles stream cleanup automatically getProperty(key, default)provides fallback values for missing keys- Sequential
load()calls merge properties with last-write-wins semantics - System properties (
-Dflags) offer a clean way to override config without touching files - Variable substitution can be implemented with a simple regex-based resolver
- Properties-to-Map conversion unlocks Groovy’s full collection API for configuration data
- ConfigSlurper is the Groovy-native alternative when you need typed values and nested blocks
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy Grape – Dependency Management Made Easy
Frequently Asked Questions
How do I read a properties file in Groovy?
Create a java.util.Properties object and use Groovy’s withInputStream to load the file safely: def props = new Properties(); new File('app.properties').withInputStream { props.load(it) }. Access values with props.getProperty('key') or the shorthand props['key']. The withInputStream method ensures the file stream is closed automatically.
What is the difference between Properties and ConfigSlurper in Groovy?
Properties is Java’s built-in key-value format – all values are strings, keys are flat (dot-separated by convention), and the format is safe since it cannot execute code. ConfigSlurper is Groovy’s DSL-based alternative that supports native types (int, boolean, list), true nested structures, and built-in environment blocks. Use Properties for Java interop and simple config; use ConfigSlurper for Groovy-only projects that need richer configuration.
How do I set default values for missing properties in Groovy?
Use the two-argument form of getProperty(): props.getProperty('db.timeout', '30000') returns '30000' if the key is missing. Alternatively, use Groovy’s elvis operator: def timeout = props['db.timeout'] ?: '30000'. The getProperty() approach only falls back when the key is absent, while the elvis operator also falls back on empty or null values.
Can I merge multiple property files in Groovy?
Yes. Call load() multiple times on the same Properties object. Each call adds new keys and overwrites existing ones with the new values. This gives you last-write-wins semantics: new File('base.properties').withInputStream { props.load(it) }; new File('override.properties').withInputStream { props.load(it) }. Keys from the override file replace matching keys from the base file.
How do I convert Groovy Properties to a Map or JSON?
Since Properties implements Map, you can convert it with collectEntries: Map map = props.collectEntries { k, v -> [k.toString(), v.toString()] }. For JSON, combine this with groovy.json.JsonOutput.toJson(map). To create a nested map from dot-separated keys, split each key on '.' and build a tree structure. The reverse – map to properties – is simply props.putAll(myMap).
Related Posts
Previous in Series: Groovy and Gradle – Build Automation Guide
Next in Series: Groovy Grape – Dependency Management Made Easy
Related Topics You Might Like:
- Groovy ConfigSlurper – DSL-Based Configuration
- Groovy File Read, Write, Delete
- Groovy JSON Parsing – Complete Guide
This post is part of the Groovy Cookbook series on TechnoScripts.com

No comment