# How to Use Variable Expansion in @Sources Annotations with Imports

> Learn variable expansion in @Sources annotations with imports in Owner. Understand runtime variable resolution using environment variables system properties and import maps.

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

---

**Owner resolves `${var}` placeholders inside `@Sources` annotations at runtime using a hierarchical lookup of environment variables, JVM system properties, and per-configuration import maps.**

The Owner configuration library (matteobaccan/owner) allows dynamic resource locations through placeholder expansion in the `@Sources` annotation. This mechanism enables environment-specific configuration loading without recompiling your code. Understanding how to combine global factory properties with per-configuration imports gives you fine-grained control over configuration resolution.

## How Variable Expansion Works in @Sources

The expansion is handled by `VariablesExpander` (see [`owner/src/main/java/org/aeonbits/owner/VariablesExpander.java`](https://github.com/matteobaccan/owner/blob/main/owner/src/main/java/org/aeonbits/owner/VariablesExpander.java)), which constructs a `StrSubstitutor` from three sources. When `ConfigFactory` creates a configuration object, it builds a `ConfigURIFactory` that calls `expander.expand(spec)` before converting the string to a `java.net.URI`.

Resolution occurs in the following priority order:

1. **System environment variables** – retrieved via `System.getenv()`
2. **JVM system properties** – retrieved via `System.getProperties()`
3. **Properties supplied to the factory** – set via `ConfigFactory.setProperty(..)`, `ConfigFactory.setProperties(..)`, or passed as imports to `ConfigFactory.create(..)`

If a placeholder cannot be resolved, it remains unchanged, causing the subsequent URI parsing to fail with a `URISyntaxException` or resource-not-found error.

## Setting Global Factory Properties

Use `ConfigFactory.setProperty()` to define variables that apply to all configuration instances created by the factory.

```java
@Sources("file:${configDir}/myapp.properties")
public interface GlobalConfig extends Config {
    String name();
}

// Set once for the entire JVM
ConfigFactory.setProperty("configDir", "/opt/config");

GlobalConfig cfg = ConfigFactory.create(GlobalConfig.class);
// Resolves to: file:/opt/config/myapp.properties

```

*Source reference:* The `@Sources` annotation is declared in [`owner/src/main/java/org/aeonbits/owner/Config.java`](https://github.com/matteobaccan/owner/blob/main/owner/src/main/java/org/aeonbits/owner/Config.java) (lines 69-71).

## Using Per-Configuration Imports

Imports are `Map<?,?>` arguments passed to `ConfigFactory.create(..)`. They merge into the `VariablesExpander` for that specific instance only, leaving other configurations unaffected.

```java
@Sources("classpath:settings-${profile}.properties")
public interface ProfileConfig extends Config {
    String url();
}

Map<String, String> dev = Collections.singletonMap("profile", "dev");
Map<String, String> prod = Collections.singletonMap("profile", "prod");

ProfileConfig devCfg = ConfigFactory.create(ProfileConfig.class, dev);
ProfileConfig prodCfg = ConfigFactory.create(ProfileConfig.class, prod);

```

*Source reference:* Import handling in `ConfigFactory.create` is implemented in [`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) (lines 65-73).

## Mixing Resolution Sources

You can combine environment variables, JVM properties, and imports in a single source specification. The `VariablesExpander` merges all sources according to the priority chain defined in [`owner/src/main/java/org/aeonbits/owner/VariablesExpander.java`](https://github.com/matteobaccan/owner/blob/main/owner/src/main/java/org/aeonbits/owner/VariablesExpander.java) (lines 31-35).

```java
// OS env: HOME=/home/alice
// JVM arg: -Denv=stage
@Sources("file:${HOME}/${env}/${path}")
public interface MixedConfig extends Config {
    String data();
}

Map<String, String> vars = Collections.singletonMap("path", "data.properties");
MixedConfig cfg = ConfigFactory.create(MixedConfig.class, vars);
// Final URI: file:/home/alice/stage/data.properties

```

## Dynamic Protocol and Path Expansion

Placeholders can contain entire URI schemes or partial paths. The `ConfigURIFactory` expands the specification before protocol handling occurs (see line 34 in [`owner/src/main/java/org/aeonbits/owner/ConfigURIFactory.java`](https://github.com/matteobaccan/owner/blob/main/owner/src/main/java/org/aeonbits/owner/ConfigURIFactory.java)).

```java
@Sources("${myurl}")
public interface UrlConfig extends Config {
    String value();
}

// Per-config import containing protocol
Map<String, String> map = Collections.singletonMap("myurl", "file:${configDir}/override.properties");
ConfigFactory.setProperty("configDir", "/tmp");

UrlConfig cfg = ConfigFactory.create(UrlConfig.class, map);
// Loads from file:/tmp/override.properties

```

## Summary

- **Variable expansion** in `@Sources` uses `${var}` syntax resolved by `VariablesExpander` at runtime.
- **Resolution priority**: environment variables → JVM system properties → factory properties → per-configuration imports.
- **Global properties** set via `ConfigFactory.setProperty()` affect all subsequent configuration instances.
- **Per-configuration imports** passed to `ConfigFactory.create(..)` provide isolated variable scopes for individual configuration objects.
- **Expansion occurs once** before URI parsing, allowing dynamic protocol selection and path construction.

## Frequently Asked Questions

### What happens if a variable in @Sources cannot be resolved?

If a placeholder lacks a matching value in the environment, system properties, factory properties, or imports, the `VariablesExpander` leaves the literal `${var}` text unchanged. This causes `ConfigURIFactory` to generate an invalid URI, resulting in a `URISyntaxException` or a "resource not found" error during configuration loading.

### Can I use variable expansion for the protocol part of a URI?

Yes. Placeholders can represent the entire URI specification or just the protocol segment. As implemented in [`ConfigURIFactory.java`](https://github.com/matteobaccan/owner/blob/main/ConfigURIFactory.java), expansion occurs before protocol-specific handling, so `@Sources("${protocol}:${path}")` correctly resolves to `file:config.properties` or `classpath:config.properties` depending on your variable definitions.

### Do imports override system properties in Owner?

No. The resolution chain follows a strict hierarchy where system environment variables and JVM system properties take precedence over factory properties and imports. According to [`VariablesExpander.java`](https://github.com/matteobaccan/owner/blob/main/VariablesExpander.java) (lines 31-35), imports are checked last, meaning they can only provide values for variables not already defined at the OS or JVM level.

### How do I debug which variables are being expanded in my @Sources?

Owner does not provide a dedicated debug mode for variable expansion, but you can inspect the resolved URI by catching loading exceptions or implementing a custom loader. Alternatively, set breakpoints in `VariablesExpander.expand()` or `ConfigURIFactory.create()` to observe the substitution process during configuration instantiation.