How to Use the Owner ConfigFactory API to Create and Manage Configuration Instances

The Owner ConfigFactory API provides a static singleton factory to instantiate type-safe configuration objects from annotated interfaces, handling property loading, variable expansion, and hot-reload capabilities.

The Owner library from the matteobaccan/owner repository eliminates boilerplate configuration code by mapping Java interfaces directly to property files. At the heart of this library lies the ConfigFactory API, which serves as the central entry point for creating and managing configuration instances. This article explores how to leverage ConfigFactory to build type-safe config objects, customize loading behavior, and handle dynamic updates.

Architecture Overview

The ConfigFactory implementation relies on several key components defined in the owner/src/main/java/org/aeonbits/owner/ package:

  • Config – The marker interface that every configuration interface must extend. It defines the annotation model (@Sources, @DefaultValue, @Key, @HotReload) and loading policies (LoadPolicy, LoadType). Source: owner/src/main/java/org/aeonbits/owner/Config.java.

  • ConfigFactory – The static singleton factory that provides the public API for creating configurations, storing global properties, and registering extensions. Key methods include create(), setProperty(), registerLoader(), and setTypeConverter(). Source: owner/src/main/java/org/aeonbits/owner/ConfigFactory.java.

  • Factory (Internal) – The actual builder implementation (hidden behind ConfigFactory) that parses @Sources annotations, applies load policies, and generates dynamic proxies. This is instantiated as a singleton INSTANCE via ConfigFactory.newInstance().

  • Loaders – Pluggable components that read specific URI protocols (e.g., file:, classpath:). Registered via ConfigFactory.registerLoader().

  • Converters – Components that transform raw String property values into custom Java types. Managed via ConfigFactory.setTypeConverter().

The factory maintains a singleton Factory instance (INSTANCE) created by ConfigFactory.newInstance(), which initializes a daemon ScheduledExecutorService for hot-reload functionality and an empty Properties object for global state.

Creating Configuration Instances

The primary entry point is ConfigFactory.create(), which validates input parameters and delegates to the internal factory to build a proxy implementation of your configuration interface.

// 1️⃣ Define the interface
@Sources("classpath:server.properties")
public interface ServerConfig extends Config {
    int port();
    String hostname();
    @DefaultValue("10")
    int maxThreads();          // default used if property missing
}

// 2️⃣ Create and use the config
public class MyApp {
    public static void main(String[] args) {
        ServerConfig cfg = ConfigFactory.create(ServerConfig.class);
        System.out.println("Server " + cfg.hostname() + ":" + cfg.port()
                           + " (maxThreads=" + cfg.maxThreads() + ')');
    }
}

In owner/src/main/java/org/aeonbits/owner/ConfigFactory.java, the create() method validates that any supplied import maps contain no null keys or values before forwarding the request to INSTANCE.create(). The factory then reads the @Sources URI(s), applies the chosen LoadPolicy, and maps interface methods to property values.

Managing Global Properties and Variable Expansion

You can define global properties that ConfigFactory uses to expand ${…} placeholders inside @Sources URIs or property values. Set these before creating your configuration instance.

@Sources("file:${confDir}/app.properties")
public interface AppConfig extends Config {
    String dbUrl();
}

// Before creating the config, set the variable that will be expanded:
ConfigFactory.setProperty("confDir", "/opt/app/conf");

// Now the file URI resolves to "/opt/app/conf/app.properties"
AppConfig cfg = ConfigFactory.create(AppConfig.class);
System.out.println(cfg.dbUrl());

The setProperty() method stores key-value pairs in the factory's global Properties object, which the internal parser consults during URI expansion. This allows you to externalize path configurations or environment-specific variables without hardcoding them in annotations.

Extending the API with Custom Loaders and Converters

The ConfigFactory API supports pluggable loaders and type converters to handle non-standard protocols and complex data types.

Registering Custom Loaders

Implement the Loader interface to support new URI schemes, then register it with the factory:

public class JsonLoader implements Loader {
    @Override public boolean accepts(URI uri) {
        return "json".equalsIgnoreCase(uri.getScheme());
    }
    @Override public void load(Properties props, URI uri) throws IOException {
        // parse JSON file and fill `props`
    }
}

// Register the loader once (typically at application start‑up)
ConfigFactory.registerLoader(new JsonLoader());

// Now you can use a JSON source:
@Sources("json:/etc/app/config.json")
public interface JsonConfig extends Config {
    String apiKey();
}

The registerLoader() method forwards the custom loader to the singleton factory, enabling resolution of proprietary or non-standard resource protocols.

Adding Custom Type Converters

For properties that map to complex Java types, register a converter:

public class InetAddressConverter implements Converter<InetAddress> {
    @Override public InetAddress convert(Method method, String value) throws Exception {
        return InetAddress.getByName(value);
    }
}

// Register the converter for InetAddress.class
ConfigFactory.setTypeConverter(InetAddress.class, InetAddressConverter.class);

@Sources("classpath:net.properties")
public interface NetConfig extends Config {
    InetAddress host();   // will be converted automatically
}

The setTypeConverter() method stores the converter in the factory's registry, allowing automatic type transformation when the configuration proxy invokes methods returning the registered type.

Enabling Hot-Reload for Dynamic Configuration

For configurations that must reflect runtime changes to underlying property files, use the @HotReload annotation. This triggers the ScheduledExecutorService daemon (created in ConfigFactory.newInstance()) to poll the file system at the specified interval.

@HotReload(2)                     // check every 2 seconds
@Sources("file:/tmp/dynamic.properties")
public interface DynamicConfig extends Config {
    String message();
}

// The config will automatically reflect changes made to the file.
DynamicConfig cfg = ConfigFactory.create(DynamicConfig.class);
while (true) {
    System.out.println("Current: " + cfg.message());
    Thread.sleep(5000);
}

The reload mechanism lives in the internal Factory implementation, which re-reads the file and updates the backing Properties object without requiring application restart.

Summary

  • Use ConfigFactory.create(MyConfig.class) to instantiate type-safe configuration objects from interfaces extending Config.
  • Set global variables with ConfigFactory.setProperty() before creation to enable ${variable} expansion in @Sources URIs.
  • Register custom Loader implementations via registerLoader() to support non-standard URI schemes like JSON or HTTP.
  • Plug in custom type converters using setTypeConverter() to map properties to complex Java types automatically.
  • Enable dynamic updates by annotating interfaces with @HotReload and specifying a poll interval in seconds.

Frequently Asked Questions

What is the difference between ConfigFactory and the Factory interface in Owner?

ConfigFactory is the static singleton facade that provides the public API, while Factory is the internal interface implemented by DefaultFactory that handles the actual proxy creation and property loading. All calls to ConfigFactory.create() delegate to the singleton INSTANCE created via ConfigFactory.newInstance().

How does ConfigFactory handle concurrent access to configuration instances?

The ConfigFactory itself is thread-safe as it uses a singleton Factory instance initialized with a thread-safe ScheduledExecutorService for hot-reload operations. Configuration proxies created by the factory are immutable and thread-safe for reading, though hot-reload updates happen asynchronously through the scheduler daemon.

Can I use multiple property sources with different protocols in one configuration interface?

Yes. The @Sources annotation accepts an array of URI strings (e.g., @Sources({"classpath:defaults.properties", "file:${user.home}/app.properties"})). The ConfigFactory processes these according to the specified LoadPolicy (FIRST, MERGE, or OVERRIDE) defined in the interface or annotation.

Where does ConfigFactory store global properties set via setProperty()?

Global properties are stored in a Properties object held by the singleton Factory instance inside ConfigFactory. These properties are consulted during URI expansion when creating configuration instances, as implemented in owner/src/main/java/org/aeonbits/owner/ConfigFactory.java.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →