Skip to content

Lifecycle and Reload

Petrus Pradella edited this page Jul 1, 2026 · 4 revisions

Lifecycle, Reload & Watching

Config.open is the only file constructor; everything else is a method on the returned handle.

Open

Config cfg = Config.open(path, codec);
LoadStatus status = cfg.lastLoadStatus();

You can also let open pick the codec from the file's extension (via CodecRegistry.defaults()), taking a String, File or Path:

Config cfg = Config.open("config.yml");        // .yml -> YamlCodec
Config cfg = Config.open(Paths.get("a.toml")); // .toml -> TomlCodec

Fail-fast — it never guesses. A missing or unregistered extension throws a CodecException instead of falling back to a default format. Pass an explicit codec — Config.open(path, codec) — to override.

Config.open never throws on a bad file — a corrupt config must not block startup:

Situation Result lastLoadStatus()
File absent empty tree; first save() creates it ABSENT
Zero-byte file empty tree EMPTY
Parse failure file copied to .bak, empty tree (file not overwritten) PARSE_FAILED_BACKED_UP
Clean parse tree loaded OK

Save

cfg.save();           // encode (with comments + key order if the codec carries them) + atomic write
cfg.saveIfDirty();    // no I/O when nothing changed since the last load/save
cfg.saveAsync();      // CompletableFuture<Void> on a shared daemon executor

Writes go through a fair per-Config lock and an atomic move (.tmp → target), so a reader never sees a torn file.

How durably each save() must land is fixed at open time by a third open argument:

Config cfg = Config.open(path, codec, BackStore.Durability.FSYNC);
BackStore.Durability Behavior
OS_CACHE (default) returns once the atomic rename is visible; the bytes may still live only in the OS page cache
FSYNC forces the bytes and the rename to the storage device before returning — slower, but survives an OS/power crash

An OS_CACHE write never corrupts the previous content (the rename is atomic); a crash can only lose the latest write. FSYNC trades throughput for surviving that crash.

In-memory save principle. Outside a watcher, a Config lives entirely in memory: save() never reads the file first — it dumps the in-memory tree. If something edits the file on disk while your app holds the config, that edit is overwritten on the next save() unless you reload() to pick it up. Editing a running app's config without a reload is an anti-pattern.

In-memory configs & changing format

Config.inMemory() builds a Config with no file but the full typed/POJO API — setValue/mergeValue, getValue(path, type), @Key/@Section/@Comment, enum-by-name, java.time, Optional. It is the right handle for a transient or test config; save() throws because there is nowhere to write.

Config cfg = Config.inMemory();
cfg.setValue("database.pool.size", 16);
DbSettings db = cfg.getValue("database", DbSettings.class);
// cfg.save();  // throws — no back-store; persist with save(codec) below

Config.inMemory() is not the same as a bare new Config(). inMemory() carries a codec, so it accepts any annotation-aware POJO; new Config() has no codec and accepts only native values (scalar / Map / list / JsonNode).

A file-backed Config can persist or re-target a different format without re-opening:

Config cfg = Config.open("server.yml");      // live codec = YamlCodec

cfg.save(new JsonCodec());                    // one-shot: write JSON to the same file; live codec unchanged
cfg.changeCodec(new TomlCodec());             // switch the format used by every subsequent save()
cfg.save();                                   // now emits TOML

save(codec) and changeCodec(codec) write through the new format but do not rename the file. Emitting a format the extension does not imply is your call — a later extension-inferred Config.open would pick the wrong codec. Switching from a comment-bearing codec (YAML/TOML/JSONC) to JSON (NONE) drops the comment overlay on the next save.

Reload

cfg.reload();
Situation Result lastLoadStatus()
Clean parse replaces the live tree OK
File absent keeps the live tree ABSENT
Parse failure keeps the live tree, no backup, no overwrite PARSE_FAILED_KEPT
cfg.isDivergedFromDisk();   // true after a PARSE_FAILED_KEPT reload (memory differs from a broken file)
cfg.getLastModified();      // last-write timestamp
cfg.hasBeenModified();      // changed on disk since the last load/save?

Watching (auto-reload)

Poll the file on a daemon thread and refresh the tree when it changes on disk:

cfg.onReload(() -> log.info("config reloaded"))
   .withAutoReload(Duration.ofSeconds(2));   // positive duration; closes any prior watcher

// ... later
cfg.stopAutoReload();   // halt polling (a self-save does NOT trigger a self-reload)

By default the watcher diffs a cheap (mtime, size) fingerprint. A second boolean argument also hashes the file content each poll, catching a same-size edit that lands within one coarse mtime tick:

cfg.withAutoReload(Duration.ofSeconds(2), true);   // also detect in-place edits

That costs a full read per poll, so it is opt-in. Leave it false (the one-arg form) unless edits that keep the byte count identical must be picked up.

Close

cfg.close();   // idempotent; stops the watcher. Config is AutoCloseable.
try (Config cfg = Config.open(path, codec)) {
    cfg.setValue("x", 1);
    cfg.save();
}   // auto-closed

→ See also The Dynamic API · Codecs & Formats

Clone this wiki locally