Overview
Property expressions allow you to reference other configuration properties within a value using the ${key} syntax. This is evaluated before type conversion and provides composition and defaults.
Basic syntax
Complex expressions
Combine multiple references in a single value:
app.db.host=localhost
app.db.port=5432
app.db.name=mydb
app.db.url=jdbc:postgresql://${app.db.host}:${app.db.port}/${app.db.name}
String dbUrl = config.getValue("app.db.url", String.class);
// Result: "jdbc:postgresql://localhost:5432/mydb"
Expressions with profiles
Expressions are resolved after profile matching:
# Base configuration
app.db.host=prod-db.example.com
%dev.app.db.host=localhost
# Expression uses the resolved value
app.db.url=jdbc:postgresql://${app.db.host}:5432/mydb
java -Dmp.config.profile=dev MyApplication
String dbUrl = config.getValue("app.db.url", String.class);
// With dev profile active: resolved host is "localhost"
// Result: "jdbc:postgresql://localhost:5432/mydb"
Cycle detection
If a property expression references itself (directly or indirectly), an IllegalArgumentException is thrown:
# Direct cycle
app.value=${app.value}
# Indirect cycle
app.a=${app.b}
app.b=${app.a}
try {
config.getValue("app.value", String.class);
} catch (IllegalArgumentException e) {
System.err.println("Cycle detected: " + e.getMessage());
}
Ravel detects cycles using a ScopedValue<Set<String>> to track currently-resolving keys.
Performance notes
-
Lazy evaluation — expressions are resolved only when the value is requested
-
No repeated resolution — the same expression within a single lookup is not re-evaluated
-
Thread-safe —
ScopedValueis virtual-thread-friendly and supports structured concurrency
Examples
Multi-level composition
company=Acme
department=Engineering
team.email=${company}-${department}@example.com
Result: team.email = Acme-Engineering@example.com
Feature flags with defaults
app.features.cache=true
app.cache.ttl=${app.cache.ttl:3600}
app.cache.maxsize=${app.cache.maxsize:1000}
Environment-specific URLs
# Base
app.api.scheme=https
app.api.host=api.example.com
app.api.port=443
app.api.url=${app.api.scheme}://${app.api.host}:${app.api.port}/v1
# Dev override
%dev.app.api.scheme=http
%dev.app.api.host=localhost
%dev.app.api.port=8080
Result (dev profile):
- ${app.api.scheme} → http
- ${app.api.host} → localhost
- ${app.api.port} → 8080
- app.api.url → http://localhost:8080/v1
Next
-
Config Profiles — environment-specific configuration
-
CDI Integration — inject resolved values