# How the DD Poker Property Configuration System Works with User Overrides

> Understand the DD Poker property configuration system. Learn how it merges hierarchical properties with user overrides, ensuring user settings take highest precedence when allowOverrides is enabled.

- Repository: [Doug Donohoe/ddpoker](https://github.com/dougdonohoe/ddpoker)
- Tags: internals
- Published: 2026-02-28

---

**The DD Poker property configuration system loads hierarchical `.properties` files and merges per-user overrides from `module/override/<username>.properties` when `allowOverrides` is enabled, with later loads overwriting earlier keys to give user settings highest precedence.**

DD Poker implements a sophisticated property configuration system in its `com.donohoedigital.config` package that allows individual users to override default application settings without modifying shared configuration files. This mechanism, centered around the `PropertyConfig` class, enables personalized gameplay experiences while maintaining sane defaults for all users.

## Initialization and the Loading Sequence

The configuration loading process begins when `ConfigManager` instantiates `PropertyConfig` during application startup. The constructor signature determines whether user overrides are permitted:

```java
// Inside ConfigManager constructor
new PropertyConfig(sAppName, modules, type, locale, allowOverrides);

```

*Source:* [[`ConfigManager.java`](https://github.com/dougdonohoe/ddpoker/blob/main/ConfigManager.java)](https://github.com/dougdonohoe/ddpoker/blob/main/code/common/src/main/java/com/donohoedigital/config/ConfigManager.java)

### The Module Iteration Loop

Inside `PropertyConfig.init()`, the loader iterates over each module (such as `"common"` or `"poker"`) and loads configuration files in a specific sequence. This loop establishes the base configuration before applying any user-specific modifications:

```java
for (String module : modules) {
    file = new File(module, PROPS_CONFIG_COMMON);
    loadURL(file, sLocale, false);          // defaults

    // type-specific file
    file = new File(module, name);
    loadURL(file, sLocale, false);

    // per-user override
    if (allowOverrides) {
        String user = ConfigUtils.getUserName();
        file = new File(module + "/override/" + user.toLowerCase() + ".properties");
        loadURL(file, sLocale, true);
    }
}

```

*Source:* [[`PropertyConfig.java`](https://github.com/dougdonohoe/ddpoker/blob/main/PropertyConfig.java)](https://github.com/dougdonohoe/ddpoker/blob/main/code/common/src/main/java/com/donohoedigital/config/PropertyConfig.java)

### Per-User Override Resolution

When the `allowOverrides` parameter is `true`, the system calls `ConfigUtils.getUserName()` to determine the current operating system user. It then constructs a file path following the pattern `module/override/<username>.properties` (with the username converted to lowercase). The `loadURL` method receives `true` for its `bOverride` parameter, triggering log messages that indicate local overrides are being applied.

## Override Precedence and Merging Strategy

The system employs a **last-write-wins** merging strategy using standard `java.util.Properties.load()` behavior. Configuration sources are loaded in the following precedence order, with each subsequent source overwriting existing keys:

1. **Base defaults**: `common.properties` for each module
2. **Application-type specific**: Files like `client.properties` or `server.properties`
3. **Per-user overrides**: `module/override/<username>.properties` (loaded only when `allowOverrides` is true)
4. **Data-directory overrides**: An optional `testing.properties` file in the user's client home directory

### Classpath Resolution and Localization

The `loadURL` method resolves resources using the pattern `classpath*:config/<path>`. When a locale is specified, the loader first searches for localized variants (e.g., `client.properties.fr`) before falling back to the base file. This resolution occurs before the override logic determines whether to apply user-specific values.

### Testing Overrides for Development

After the module loop completes, the system checks for ad-hoc overrides intended for unit tests or development tweaks:

```java
RuntimeDirectory dir = new DefaultRuntimeDirectory();
File userdir = dir.getClientHome(appName);
File override = new File(userdir, "testing.properties");
if (override.exists()) {
    load(override);   // forces the testing file on top
}

```

*Source:* [[`PropertyConfig.java`](https://github.com/dougdonohoe/ddpoker/blob/main/PropertyConfig.java)](https://github.com/dougdonohoe/ddpoker/blob/main/code/common/src/main/java/com/donohoedigital/config/PropertyConfig.java) and [[`DefaultRuntimeDirectory.java`](https://github.com/dougdonohoe/ddpoker/blob/main/DefaultRuntimeDirectory.java)](https://github.com/dougdonohoe/ddpoker/blob/main/code/common/src/main/java/com/donohoedigital/config/DefaultRuntimeDirectory.java)

## Accessing Merged Configuration Values

Once initialization completes, the merged `Properties` object resides in a singleton `propConfig` instance. All application code accesses values through static helper methods that provide type conversion and default value handling:

```java
String maxPlayers = PropertyConfig.getStringProperty("game.maxplayers", "8");
int timeoutSec   = PropertyConfig.getIntegerProperty("network.timeout", 30);
boolean debug    = PropertyConfig.getBooleanProperty("debug.enabled", false);

```

These methods read from the already-merged configuration, ensuring that user overrides are transparent to the calling code.

## Practical Example: Creating a User Override

Consider the default configuration in `poker/client.properties`:

```properties
game.maxplayers=8

```

A user named **Alice** wishes to increase the maximum to 12 seats. She creates the file `poker/override/alice.properties` with the following content:

```properties
game.maxplayers=12

```

When the application starts with `new ConfigManager("poker", ApplicationType.CLIENT, true)`, the loading sequence executes as follows:

1. Loads `poker/common.properties` and `poker/client.properties`, setting `game.maxplayers` to `8`
2. Detects `allowOverrides=true` and locates `poker/override/alice.properties`
3. Calls `loadURL(..., true)`, logging the override application
4. The key `game.maxplayers` is overwritten to `12`

Subsequent calls to `PropertyConfig.getIntegerProperty("game.maxplayers", ...)` return `12` for Alice, while other users continue to receive the default value of `8`.

## Summary

- The property configuration system loads base files (`common.properties`, type-specific files) for each module before checking for user overrides.
- When `allowOverrides` is enabled, the system loads per-user files from `module/override/<username>.properties`, with usernames normalized to lowercase via `ConfigUtils.getUserName()`.
- The standard `java.util.Properties.load()` mechanism ensures that later configuration sources overwrite earlier ones, establishing a clear precedence hierarchy.
- An optional `testing.properties` file in the client home directory can supersede all other values for development purposes.
- Runtime code accesses the merged configuration through static methods like `getStringProperty()` and `getIntegerProperty()` without needing to understand the override hierarchy.

## Frequently Asked Questions

### How does the system determine which user override file to load?

The system calls `ConfigUtils.getUserName()` to obtain the current operating system username, converts it to lowercase, and appends `.properties` to construct the filename. It then searches for this file within each module's `override` directory using the path pattern `module/override/<username>.properties`.

### Can user overrides modify any configuration property?

Yes, the property configuration system treats all keys equally during the merge process. Any property defined in the base configuration files can be overwritten in a per-user override file, including network timeouts, game settings, or debug flags.

### What happens if the user override file does not exist?

If the file does not exist, `loadURL()` simply returns without error, and the system continues using the values loaded from the default configuration files. This allows `allowOverrides` to remain enabled globally without requiring every user to create an override file.

### How do localized property files interact with user overrides?

Localization occurs at the `loadURL` level before override logic applies. For a given base file, the system first attempts to load the locale-specific variant (e.g., `client.properties.fr`). The per-user override file then loads on top of whatever localized version was resolved, allowing users to override both default and localized values.