# How to Implement a Custom ZooKeeper Loader from the Owner-Extras Module

> Learn to implement a custom ZooKeeper loader in OWNER. Handle zookeeper URIs, extract properties using Apache Curator, and register your loader for flexible configuration management.

- Repository: [Matteo Baccan/owner](https://github.com/matteobaccan/owner)
- Tags: how-to-guide
- Published: 2026-03-07

---

**Implement the `Loader` interface from `org.aeonbits.owner.loaders`, handle the `zookeeper` URI scheme in `accept(URI)`, extract properties using Apache Curator in `load(Properties, URI)`, and register your class via `ConfigFactory.registerLoader()` before creating the configuration instance.**

The **owner-extras** module in the matteobaccan/owner repository ships with a reference `ZooKeeperLoader` that demonstrates how to bridge Owner’s type-safe configuration interfaces with Apache ZooKeeper. Creating a custom ZooKeeper loader lets you customize node traversal, connection retry policies, and serialization while preserving the clean `@Sources` annotation-driven configuration model.

## Understanding the Loader Contract in Owner

Every configuration loader in the Owner framework must implement the **`Loader`** interface located at [`owner/src/main/java/org/aeonbits/owner/loaders/Loader.java`](https://github.com/matteobaccan/owner/blob/main/owner/src/main/java/org/aeonbits/owner/loaders/Loader.java). This contract defines three methods that control how Owner resolves and reads external configuration sources:

- **`boolean accept(URI uri)`** – Returns `true` only for URIs your loader can handle (e.g., scheme `zookeeper`).
- **`void load(Properties result, URI uri)`** – Connects to the external source, reads data, and populates the supplied `Properties` object.
- **`String defaultSpecFor(String urlPrefix)`** – Optionally returns a default URL when none is declared in `@Sources`; return `null` if unsupported.

The **Properties** object passed into `load` acts as a mutable map; keys and values you insert become accessible through the typed configuration interface.

## Anatomy of the Built-In ZooKeeperLoader

The reference implementation in [`owner-extras/src/main/java/org/aeonbits/owner/loaders/ZooKeeperLoader.java`](https://github.com/matteobaccan/owner/blob/main/owner-extras/src/main/java/org/aeonbits/owner/loaders/ZooKeeperLoader.java) demonstrates the canonical pattern for ZooKeeper integration. Study its structure before extending it:

1. **Scheme detection** – The `accept` method checks `uri.getScheme().equals("zookeeper")`.
2. **Client construction** – The private `getClient(URI)` method builds a Curator **`CuratorFramework`** using the host and port from the URI.
3. **Connection lifecycle** – Inside `load`, the client starts with `client.start()` and blocks until connected using `client.blockUntilConnected(timeout, TimeUnit.SECONDS)`, respecting the system property `owner.zookeeper.connection.timeout.seconds` (default 30 seconds).
4. **Node iteration** – The loader lists children of the base path (`uri.getPath`), retrieves data for each child via `client.getData().forPath()`, and stores entries in `result.put(key, value)`.
5. **Resource cleanup** – The client closes in a `finally` block to prevent connection leaks.

If the base path does not exist, the loader returns an empty `Properties` map, causing all configuration methods to yield `null`.

## Creating Your Custom ZooKeeper Loader

To build a specialized loader—perhaps one that supports recursive node traversal or custom authentication—follow this implementation pattern.

### Step 1: Implement the Loader Interface

Create a new class that implements `org.aeonbits.owner.loaders.Loader`. Import `org.apache.curator.framework.CuratorFramework` and `CuratorFrameworkFactory` to handle ZooKeeper connectivity.

```java
package com.example.config;

import org.aeonbits.owner.loaders.Loader;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.utils.ZKPaths;
import java.io.IOException;
import java.net.URI;
import java.util.Properties;

public class RecursiveZooKeeperLoader implements Loader {
    private static final String SCHEME = "zookeeper";

    @Override
    public boolean accept(URI uri) {
        return SCHEME.equals(uri.getScheme());
    }
    
    // ... remaining methods
}

```

### Step 2: Define Scheme Acceptance

The **`accept`** method must strictly filter URIs to avoid conflicts with other loaders. Return `true` only when the scheme matches your target protocol.

```java
@Override
public boolean accept(URI uri) {
    return "zookeeper".equals(uri.getScheme());
}

```

### Step 3: Implement Property Loading

In **`load(Properties result, URI uri)`**, instantiate the Curator client, connect, and populate the properties map. Always wrap the connection in a try-finally block to ensure `client.close()` runs.

```java
@Override
public void load(Properties result, URI uri) throws IOException {
    String connectString = uri.getHost() + (uri.getPort() == -1 ? "" : ":" + uri.getPort());
    CuratorFramework client = CuratorFrameworkFactory.newClient(connectString, 
        (retry, elapsed, sleeper) -> false);
    
    try {
        client.start();
        client.blockUntilConnected(30, java.util.concurrent.TimeUnit.SECONDS);
        
        String basePath = uri.getPath();
        for (String child : client.getChildren().forPath(basePath)) {
            String fullPath = ZKPaths.makePath(basePath, child);
            byte[] data = client.getData().forPath(fullPath);
            if (data != null) {
                result.put(child, new String(data));
            }
        }
    } catch (Exception e) {
        throw new IOException("Failed to load from ZooKeeper: " + uri, e);
    } finally {
        client.close();
    }
}

```

### Step 4: Optional Default URL Support

Override **`defaultSpecFor`** only if your loader should supply a fallback URL when the `@Sources` annotation is omitted. The built-in `ZooKeeperLoader` returns `null`, forcing explicit source declaration.

```java
@Override
public String defaultSpecFor(String urlPrefix) {
    return null; // No default ZooKeeper ensemble
}

```

## Registering and Consuming the Loader

Before creating a configuration instance, you must register your custom loader with a **`ConfigFactory`**. The factory located at [`owner/src/main/java/org/aeonbits/owner/ConfigFactory.java`](https://github.com/matteobaccan/owner/blob/main/owner/src/main/java/org/aeonbits/owner/ConfigFactory.java) maintains a registry of loaders and dispatches to them based on URI schemes.

```java
import org.aeonbits.owner.ConfigFactory;

// Define the configuration contract
@Sources("zookeeper://127.0.0.1:2181/appConfig")
public interface AppConfig extends Config {
    String databaseUrl();
    Integer connectionPoolSize();
}

// Register the custom loader and instantiate
ConfigFactory factory = ConfigFactory.newInstance();
factory.registerLoader(new RecursiveZooKeeperLoader());

AppConfig cfg = factory.create(AppConfig.class);
System.out.println(cfg.databaseUrl());

```

The test suite in [`owner-extras/src/test/java/org/aeonbits/owner/loaders/ZooKeeperLoaderTest.java`](https://github.com/matteobaccan/owner/blob/main/owner-extras/src/test/java/org/aeonbits/owner/loaders/ZooKeeperLoaderTest.java) demonstrates this registration flow using an embedded ZooKeeper server, verifying that properties correctly map from ZNodes to interface methods.

## Summary

- Implement **`org.aeonbits.owner.loaders.Loader`** and handle the `zookeeper` scheme in `accept(URI)`.
- Use **Apache Curator** (`CuratorFramework`) to connect, read children nodes, and populate the `Properties` map inside `load(Properties, URI)`.
- Close the Curator client in a `finally` block to prevent connection leaks.
- Register your implementation via **`ConfigFactory.registerLoader()`** before calling `create(Class)`.
- Return `null` from `defaultSpecFor` unless you require a default ZooKeeper connection string.

## Frequently Asked Questions

### What interface must a custom ZooKeeper loader implement?

Any custom loader must implement **`org.aeonbits.owner.loaders.Loader`**, defining `accept(URI)`, `load(Properties, URI)`, and `defaultSpecFor(String)`. This interface allows Owner to delegate URI resolution and property extraction to your code.

### How do I register my custom loader with the Owner framework?

Call **`ConfigFactory.newInstance().registerLoader(new YourLoader())`** prior to creating the configuration interface. The factory caches registered loaders and routes `@Sources` URIs to the first loader whose `accept` method returns `true`.

### Can I configure the ZooKeeper connection timeout in my custom loader?

Yes. You can respect the standard property `owner.zookeeper.connection.timeout.seconds` (default 30 seconds) as shown in the reference `ZooKeeperLoader`, or implement custom timeout logic inside `load` by adjusting the parameters passed to `client.blockUntilConnected()`.

### What happens if the ZooKeeper base path does not exist?

The loader returns an empty **`Properties`** map. Consequently, all methods on the configuration interface return `null` (or default values if specified via `@DefaultValue`), and no exception is thrown during proxy creation.