Skip to content

Architecture Overview

Petrus Pradella edited this page Jun 28, 2026 · 6 revisions

Architecture Overview

EveryConfig is a thin set of layers over one canonical Jackson tree.

        text (yaml / json / toml / jsonc)
                 │   ▲
       readTree  │   │  writeWithComments / writeTreePlain
                 ▼   │
   ┌─────────────────────────────┐     ┌──────────────────────────┐
   │  Codec (codec.jackson.*)     │     │  CommentTree (overlay)    │
   │  text ⇄ ObjectNode + mapper  │     │  block/side/header/footer │
   └─────────────────────────────┘     └──────────────────────────┘
                 │   ▲                              ▲
                 ▼   │                              │
   ┌──────────────────────────────────────────────────────────────┐
   │  Config — the canonical ObjectNode + dynamic path API          │
   │  (setValue / getValue / typed getters / getOrSetValueIfAbsent)  │
   └──────────────────────────────────────────────────────────────┘
                 │                                  │
        EntityBinder (derived, merges)        BackStore / io (atomic file I/O)

The pieces

Layer Package Role
Config config, config.section The handle: the canonical ObjectNode, the dynamic path API, the comment API, and the file lifecycle.
Core model core.tree, core.coerce, core.comment, core DPath/PathOptions (dotted-path utils — the separator is fixed at ., a dot inside a key is escaped a\.b), NodeCoercion (the single Java ⇄ Jackson seam), CommentTree + CommentType, KeyOrder.
Codec codec, codec.jackson The Codec SPI + CommentFidelity + registry + mapper profiles; the four format codecs plus the no-file InMemoryCodec.
I/O io BackStore SPI + AtomicFileBackStore: atomic write, .bak rescue, reload, poll watcher, async executor.
Binding binding, binding.schema, binding.merge, binding.introspect The typed view: EntityBinder, the schema model, smart-merge + @KeyIndex indexing (KeyIndexer), the Jackson annotation bridge.
Annotations annotation @Key, @Comment, @Section, @KeyIndex, @PostLoad (+ KeyTransformCase, CommentMode).

The 5 design decisions

These are the invariants every layer obeys.

  1. The tree is canonical. The dynamic API operates on the ObjectNode. Typed binding is a derived view; on conflict the tree wins (unknown file keys survive, FAIL_ON_UNKNOWN_PROPERTIES is off), and a binding save merges into the tree, never replaces it.
  2. Comments = seed / override. @Comment and getOrSetValueIfAbsent(path, def, comment) write comments in two explicit modes — rewrite-every-save (OVERRIDE) or write-once (SET_IF_ABSENT). See Default Values & Comments.
  3. Comment fidelity is a codec capability. Each codec declares LOSSLESS / LOSSY / NONE. JSON is NONE. See Codecs & Formats.
  4. Save = reconciliation. Load captures the (data tree, CommentTree, KeyOrder); the emitter reconciles per path and emits in file order, appending new keys.
  5. The emitter renders structure itself (keys, indent, sections, comment lines) and delegates only leaf-value serialization to the ObjectMapper — the mapper's output is never re-parsed, so a custom mapper can restyle a value but cannot break the layout. The Codec (text⇄tree⇄entity) is separate from the I/O layer (atomic file writes).

One shared mapper per codec

A Codec holds a single, thread-safe Jackson ObjectMapper, shared across every live Config of that format. There is no per-file engine, no pool, no ThreadLocal — a config's footprint is just its tree.

A Config without a file, or in another format

Because the tree is canonical and the codec is just the text⇄tree⇄entity seam, a Config need not be backed by a file at all, and its format can be swapped at runtime.

  • Config.inMemory() binds an InMemoryCodec: the full typed/POJO API works (setValue-merge, getValue(path, type), @Key/@Section/@Comment, enum-by-name, java.time, Optional) but there is no text format, so save() throws. (A bare new Config() has no codec at all and accepts only native scalar/Map/list/JsonNode values.)
  • save(Codec) persists the current tree once through a different codec, leaving the live codec untouched — e.g. open a .yml and dump a JSON snapshot.
  • changeCodec(Codec) switches the format every later save() uses, rebinding the POJO coercion to the new mapper.
Config cfg = Config.open("server.yml");           // YAML on disk
cfg.save(new JsonCodec());                          // one-shot JSON snapshot, codec still YAML
cfg.changeCodec(new TomlCodec());                   // every future save() now emits TOML

Dot-in-key. The path separator is fixed at .; a key that itself contains a dot is escaped: cfg.getInt("rates.usd\\.brl") reads the single key "usd.brl" under "rates". \\ is a literal backslash, and the escaping is a no-op for an ordinary dotted path.

→ See also Project Layout · The Dynamic API · Entity Binding · @KeyIndex Collections

Clone this wiki locally