Skip to content

Limitations

What this module does not do. Some of these are deliberate, some are the honest edge of a guarantee, and a few are simply not built yet. Each says which.

Everything here is stated because a constraint nobody wrote down is indistinguishable from a bug you have not hit yet.

Writes

There is no cross-source transaction

A write spanning several sources is prepared everywhere, verified everywhere, then committed everywhere. That narrows the window in which a half-applied set can be observed to the duration of the renames, and everything likely to fail has already happened by then — but it is not a transaction. A commit that fails partway and cannot be rolled back returns ErrPartialCommit naming what is in which state.

Real transactionality needs a journal, which is not built. Deliberate, for now.

A conflict is detected, not prevented

Another process writing the same file between routing and commit is caught by comparing a content hash, and the write is refused with ErrConflict. Nothing stops the other process from writing. Lock files would, and are not portable enough to be worth the failure modes they add. Deliberate.

Some layers can never be written to

Layer Why
Environment variables A write would not survive the process.
Command-line flags A flag lives for one invocation.
WithReaders sources In-memory; there is nowhere to persist to.
AddLayer layers Same, and they sit at the top of the order, so a write lands beneath and is reported as shadowed.
A nested store Read-only unless built with NestedPromotable, and even then only reachable by naming it with To.

Routing skips all of them. A store built only from these returns ErrNoWritableLayer for any change. Deliberate.

Setting a whole map replaces the subtree

Comments, anchors and block styles within the replaced subtree may not survive. That is what supplying a whole map asserts — deep-merging it instead would make "this subtree is now exactly this" inexpressible. If you know what changed, issue targeted Set and Remove calls instead. Deliberate; see What survives a write.

A write is not byte-for-byte reproducible

Guaranteed to survive: the data, comments attached to their keys, key order, quoting style, block scalars, anchors, aliases and merge keys. Not guaranteed: blank lines, indentation, comment alignment, the --- marker on a single-document file, or byte identity. Flow style may be normalised to block style. Deliberate — guaranteeing byte-level identity would mean never being able to fix anything about layout.

Key casing is not preserved for new keys

A key this module creates is written lower-cased, because keys are lower-cased on the way in. An existing key keeps the spelling the file has, since a write edits the existing node. So a file mixing Server: (yours) and log: (ours) is the expected outcome of writing a new section into a camel-cased file. A consequence of case-insensitive keys, not a choice made separately.

Some documents are refused before you can write to them

A multi-line flow collection with interior comments cannot be round-tripped safely, so NewStore refuses the source with ErrBackendUnsafe rather than letting you discover it at commit time. Deliberate — failing at load is the kinder failure.

Reading

Provenance is leaf-only

Origin answers for a leaf value. A populated mapping is assembled from however many layers contributed to it, so naming a single source for it would be dishonest; Origin returns false and Explain says it is "a subtree assembled from" the sources involved. Ask Shadowed instead. Deliberate.

Explain renders the value

Explain("db.password") puts the value in the string it returns. It is a diagnostic, not a redacting formatter, and this module does not know which of your keys are secret. Do not log Explain output for a key a secrets backend supplies. A known gap; use Origin and Shadowed when you need provenance without the value.

A key containing a literal dot cannot be addressed

Paths split on . unconditionally and there is no escape syntax. Keys() will list such a key; nothing else can reach it. A known gap; see Keys and paths.

List elements cannot be addressed by index

There is no servers.0.host. Read the sequence and index it in Go, or decode it into a struct. Deliberate — paths address mappings.

Sequences replace; they never merge

An overlay that mentions a list at all replaces it entirely. There is no append, no de-duplicate, and no per-index override. Deliberate; see Precedence & merge model.

Coherence is per-view, not global

A View is pinned to one snapshot, so a sequence of reads through it cannot straddle a reload. Two separate store.View() calls can. Use Store.With for a block of reads that must agree with each other. Deliberate — a handle held indefinitely would serve values that quietly grew arbitrarily old.

Validation

Only required is honoured in the validate tag

min, max, oneof, gte and every other spelling from other validation libraries are parsed and discarded without warning. enum and required are the whole vocabulary. A known gap; write the rest as your own check.

required cannot mean non-zero

A bool set to false and an int set to 0 are configured values and satisfy required. Only an absent key — or a present-but-empty string — fails. Deliberate; judging by zero-ness would reject an operator deliberately turning something off.

The type model is coarse

Unsigned integers, slices, maps, time.Time and structs all map to the schema type string, so a validated struct with a uint16 or a []string field fails its own type check. Use int and leave collections out of the validated struct. A known gap; see Struct tags.

Values from the environment or a flag are strings, and validation says so

An int field routinely supplied by MYTOOL_SERVER_PORT fails with expected type int but got string. Reading is unaffected; validation is not a reading path. Either leave the key out of the typed schema or declare it a string. Deliberate — the alternative is a schema that cannot tell 8080 from "8080" when it matters.

default: never sets anything

The tag is recorded on the field schema and used for nothing but documentation. Defaults belong in exactly one place — a WithReaders layer — because a second source of defaults will drift from the first. Deliberate.

description: is recorded and unused

The tag is parsed onto FieldSchema.Description and no message this module produces reads it. A known gap.

config:"-" does not exclude a struct field

- and "no tag at all" take the same branch, and that branch walks a struct. A struct field tagged config:"-" still contributes its children under its lower-cased field name. Leave such a field out of the validated struct instead. A known gap; see Struct tags.

Hot-reload

Only file-backed and watchable backends are covered

Environment variables and flags do not change under a running process, and an in-memory source is changed by the code that owns it. Store.Reload re-reads everything; Store.Watch covers what can report its own changes. Deliberate.

Settling does not make a multi-file change atomic

Writes spaced further apart than the settle window are seen as separate changes. The guarantee belongs to whoever writes the files. Deliberate — timing is the last resort, used only where the filesystem genuinely offers no other information.

You cannot write configuration from inside an observer

Apply, AddLayer and Reload all return ErrWriteFromObserver when called from an observer callback. Record what is needed, return, and write from elsewhere. Deliberate — the cascade has no natural end, and the store cannot break it without either dropping notifications or silently reordering what changed when.

Observers do not fire at startup

Section delivery is change-only. ObservedSection.ApplyInitial exists if you want startup delivery. Deliberate.

Composition and backends

A store appears at most once in a graph

Not just "no cycles" — two copies of one store would contribute their layers at two precedences, so every value would be shadowed by a copy of itself. ErrCyclicStore at construction. Deliberate.

An outer store does not see writes made directly to the inner one

Not until it next reloads — on the next tick when watching, or on an explicit Store.Reload. Deliberate.

A backend cannot be removed after construction

AddLayer adds a source at the highest precedence, and it survives reloads. There is no public way to withdraw it, or to remove any backend the store was built with. Build a new store. A known gap.

Filtering is not a security boundary

Allow and Deny bound what a backend contributes, after it has fetched and parsed. A denied key still crosses the network and still sits in memory briefly. Use the remote system's own access control to stop a value being fetched. Deliberate; the filter's job is visibility and write routing.

Comment preservation is document-backend-only

Capabilities.PreservesComments is meaningful for a file; a key–value store has nowhere to put a comment. Do not assume a round-trip through a parameter store keeps anything but the value. Inherent.

Only YAML is built in

Every other format — JSON, TOML, HCL, XML, dotenv, INI, Java properties — is a separate adapter module, and so is every filesystem beyond local disk and every remote backend. That is what keeps a consumer reading TOML from compiling an XML parser. Deliberate; see the adapter ecosystem.