How to Load Multiple Property Files with the MERGE Load Policy in Owner
Use the @LoadPolicy(LoadType.MERGE) annotation on your configuration interface to aggregate multiple property sources into a unified configuration, where values from the first declared source take precedence over subsequent ones.
The Owner library (matteobaccan/owner) provides a type-safe mechanism for mapping Java properties to interfaces via annotations. When you need to compose configuration from several property sources—such as layering default settings with environment-specific overrides—the MERGE load policy combines all declared files while enforcing a predictable precedence order.
Understanding the MERGE Load Policy
The MERGE load policy instructs Owner to load every URI specified in the @Sources annotation and merge them into a single Properties instance. Unlike the FIRST policy, which stops after locating the first available source, MERGE ensures that all sources contribute to the final configuration, with explicit rules for handling key collisions.
How the Merging Algorithm Works
The implementation resides in owner/src/main/java/org/aeonbits/owner/Config.java, where the LoadType.MERGE enum defines the loading strategy. The algorithm iterates over the source list in reverse order using reverse(uris), meaning it processes the last declared source first. Each source is loaded via LoadersManager.load(result, uri), and because earlier sources overwrite later ones, the first source declared in your @Sources annotation holds the highest precedence.
If a source cannot be read (for example, a missing file or inaccessible URL), Owner silently ignores that source and continues processing the remaining entries.
Implementing MERGE in Your Configuration Interface
To load multiple property files with the MERGE load policy, define your configuration interface with both @Sources and @LoadPolicy(LoadType.MERGE):
@Sources({
"classpath:org/aeonbits/owner/first.properties",
"classpath:org/aeonbits/owner/second.properties",
"file:${user.dir}/src/test/resources/org/aeonbits/owner/third.properties"
})
@LoadPolicy(LoadType.MERGE)
public interface MergeConfig extends Config {
@DefaultValue("ignored")
String foo(); // Defined in first.properties → wins
@DefaultValue("ignored")
String bar(); // Defined in second.properties → wins
@DefaultValue("theDefaultValue")
String fubar(); // Not defined in any source → default used
String quux(); // Not defined in any source → null
}
Instantiate the configuration using ConfigFactory:
MergeConfig cfg = ConfigFactory.create(MergeConfig.class);
System.out.println(cfg.foo()); // Prints value from first.properties
System.out.println(cfg.bar()); // Prints value from second.properties
System.out.println(cfg.fubar()); // Prints "theDefaultValue"
This behavior is validated in owner/src/test/java/org/aeonbits/owner/loadstrategies/MergeLoadStrategyTest.java, which demonstrates how the merge strategy handles multiple property files and precedence rules.
Precedence and Conflict Resolution
When identical keys exist across multiple property files, the value from the first declared source in the @Sources array wins. Because Owner processes the array in reverse, the first source you list becomes the last one loaded, overwriting any previous values for matching keys.
For instance, if both first.properties and second.properties define database.url, the value from first.properties takes precedence because it appears first in the annotation.
Handling Missing Properties and Defaults
When a property key is absent from all merged sources, Owner resolves the value through the following fallback mechanism:
- Method-level default: If the method specifies
@DefaultValue("value"), that value is returned. - Null return: If no default annotation is present, the method returns
null(or the default value for the return type if primitives are involved).
This allows optional configuration parameters to specify safe defaults while required parameters can remain unmapped to trigger validation logic.
Error Handling with Invalid Sources
While Owner silently ignores unreadable sources such as missing files, it strictly validates URI schemes. Specifying a malformed or unsupported protocol results in an UnsupportedOperationException during configuration creation:
@Sources("httpz://foo.bar.baz") // Invalid scheme
@LoadPolicy(LoadType.MERGE)
interface InvalidURLConfig extends Config {}
// Throws UnsupportedOperationException at runtime
ConfigFactory.create(InvalidURLConfig.class);
Ensure your source URLs use supported protocols (classpath, file, http, https, or environment variables) to avoid runtime failures.
Summary
- The
@LoadPolicy(LoadType.MERGE)annotation aggregates all declared property sources into a single configuration object. - Sources are processed in reverse order, giving the first declared source in
@Sourcesthe highest precedence for conflicting keys. - Unreadable sources are silently skipped, but invalid URI schemes throw
UnsupportedOperationException. - Missing keys fall back to
@DefaultValueannotations or returnnull. - The core logic is implemented in
Config.javawith comprehensive test coverage inMergeLoadStrategyTest.java.
Frequently Asked Questions
What is the difference between MERGE and FIRST load policies in Owner?
The FIRST load policy stops after successfully loading the first available source from the @Sources list, treating it as the complete configuration. The MERGE policy loads all sources and combines them into a unified Properties object. Use FIRST for failover scenarios where you want the first existing file, and use MERGE for layering configurations where multiple files contribute values.
How does Owner determine which value to use when the same key appears in multiple files?
Owner processes the @Sources array in reverse order (from last to first), so the first source declared in your annotation overwrites values loaded from subsequent sources. This reverse iteration ensures that the earliest source in your declaration has the final word on conflicting property values.
What happens if one of the property files is missing when using MERGE?
If a source specified in @Sources cannot be read—for example, a file does not exist or a classpath resource is missing—Owner silently ignores that specific source and continues loading the remaining sources. This behavior allows you to specify optional configuration layers without causing the application to fail.
Can I mix different source types in a single MERGE configuration?
Yes, the @Sources annotation accepts any combination of valid URIs, including classpath: resources, absolute or relative file: paths, and remote URLs. Owner's LoadersManager handles each protocol appropriately during the merge process, combining all successfully loaded sources regardless of their storage location.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →