Dropwizard Configuration: Merging, Validation, and Secrets in Practice
Dropwizard's YAML configuration merges multiple files, validates with Bean Validation, and substitutes environment variables — but secrets can leak via the admin endpoint. Learn the merging order, validation hooks, and a practical pattern to keep production configs safe.
30 Jul 2025, 08:36 UTC

The problem: config that works locally but breaks in production
You add a new required field to config.yml, push the change, and the service starts fine on your laptop. In the staging pipeline the same jar fails with a ConfigurationException because the staging YAML is missing the field. The root cause is not a code bug — it is how Dropwizard merges multiple YAML files, validates them, and exposes the resolved values.
Understanding the configuration subsystem lets you catch those mismatches at build time, keep secrets out of logs, and avoid the "works on my machine" surprise.
Core model: a single POJO backed by Bean Validation
Dropwizard expects a subclass of io.dropwizard.Configuration. Each field maps to a YAML key, and Jakarta Bean Validation annotations (@NotNull, @Min, @Valid, etc.) declare constraints. At startup ConfigurationFactory parses the YAML, substitutes environment variables (${ENV_VAR}), runs validation, and either returns a fully populated configuration instance or throws a detailed ConfigurationException that includes the property path and message.
public class AppConfig extends Configuration {
@NotNull
@Valid
private DataSourceFactory database = new DataSourceFactory();
@NotNull
private String serviceName;
// getters / setters omitted
}
Nested factories like DataSourceFactory are validated recursively when annotated with @Valid, giving you type-safe database, HTTP client, and custom component settings without extra parsing code. Note the namespace: Dropwizard 4.x uses jakarta.validation.*, while 2.x/3.x use javax.validation.*. Mixing them causes constraints to be silently skipped.
Merging environment-specific overrides
Dropwizard merges YAML files in the order they appear on the command line. A typical layout:
config.yml– base settings shared everywhereconfig-dev.yml– developer overrides (e.g., local DB URL)config-prod.yml– production overrides (e.g., pool size, timeouts)
Run the service with:
java -jar app.jar server config.yml config-prod.yml
Later files win. In the example above, config-prod.yml might only contain:
database:
url: "jdbc:postgresql://prod-db:5432/app"
maxSize: 30
All other keys (driver class, user, etc.) come from config.yml. This keeps duplication low while still allowing per-environment tuning.
Validation that stops bad deploys
Because validation runs before any application code executes, a missing serviceName or an invalid maxSize (e.g., negative) aborts startup with a clear message identifying the property path:
ConfigurationException: config.yml has an error:
* serviceName may not be null
* database.maxSize must be greater than or equal to 1
You can add cross-field rules by implementing validate() in your Configuration subclass or by registering a custom ValidatorFactory in Application.initialize(Bootstrap) — for example, to enforce startDate < endDate. The validator runs during the same startup phase, so the service never reaches a running state with an impossible date range.
Secrets, the admin endpoint, and a practical limitation
Environment variable substitution works only for scalar string values in YAML. You cannot inject directly into a list or map. For secrets, the common pattern is:
- Store the secret in the runtime environment (e.g.,
DB_PASSWORD=supersecret). - Reference it in YAML:
password: ${DB_PASSWORD}. - Redact sensitive keys on the admin endpoint (
/configon port 8081), which otherwise shows the full resolved configuration, by providing a customConfigurationFilter.
Dropwizard does not encrypt values in the YAML files themselves. If you need encrypted files, integrate a custom ConfigurationSourceProvider that decrypts, or pull secrets from Vault or a similar store at startup.
Limitation: By default ConfigurationFactory fails fast on unknown YAML properties. Setting setAllowUnknownProperties(true) can ease migrations but hides typos in production configs — use it only temporarily.
Worked example: verify a merged, validated config
Assume the following files exist in the project root:
# config.yml
database:
driverClass: org.postgresql.Driver
user: app_user
password: ${DB_PASSWORD}
url: jdbc:postgresql://localhost:5432/app
maxSize: 10
serviceName: my-service
# config-prod.yml
database:
url: jdbc:postgresql://prod-db:5432/app
maxSize: 30
Set the environment variable and start (run from the project directory, with a shell that can export variables):
export DB_PASSWORD=prod-secret
java -jar target/app.jar server config.yml config-prod.yml
Expected checks:
- The process starts without a
ConfigurationException. - A health check that logs
configuration.getDatabase().getUrl()prints the production URL, confirming the merge order. - The admin endpoint
http://localhost:8081/configshows the merged config withpasswordredacted (if you added a filter).
If you omit serviceName from both files, the startup fails with a precise path (serviceName may not be null), confirming validation works. This example reflects Dropwizard 4.x behavior; verify against your version's migration notes if upgrading from 3.x.
Actionable closing
1. Keep a single base config.yml and one override per environment. 2. Annotate every required field with @NotNull and nested factories with @Valid. 3. Run the jar with the full file list in your CI pipeline to catch missing keys early. 4. Use environment variables for secrets and add a ConfigurationFilter to keep them off the admin endpoint. 5. Only enable allowUnknownProperties during a controlled migration, then turn it off.
With these habits, configuration drift becomes a build-time error instead of a runtime outage.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.