Groovy Property Files – Read, Write, and Manage Config with 10 Tested Examples

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.

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 .properties files. 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 .properties files.

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 withInputStream for 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 (-D flags) 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).

Previous in Series: Groovy and Gradle – Build Automation Guide

Next in Series: Groovy Grape – Dependency Management Made Easy

Related Topics You Might Like:

This post is part of the Groovy Cookbook series on TechnoScripts.com

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *