ADR-014: A storage write goes to a temporary file beside the target and then replaces it
Date: 2026-08-03
Status: Accepted
Context
components/storage carries a payload to a platform without interpreting it. The hosted
implementation writes over a file the user already owns, and that file is the only copy of a pet.
SPEC-NFR-004 asks the product to fail without losing state,
and a write that opens the target and then fails part way through loses exactly the state it was
asked to preserve.
The failure is not hypothetical. A full disk, a process killed during a write, and a power loss on a desktop all leave a partially written file where a valid one was, and the Core then rejects the payload on the next load because its length or its integrity check no longer holds. The pet is gone, and the format did its job by refusing to load rubbish.
The implementation is also the one place in this release that has to name platform behaviour that ISO C leaves open, which is why the choice belongs in a record rather than in a comment.
Decision
A write never modifies the target. The implementation writes the whole payload to a temporary file beside the target and replaces the target with it once the temporary file is closed successfully.
The temporary name is the target name with .tmp appended, so it lives in the target’s directory
rather than in a system temporary directory. A rename across filesystems is not a rename, and a
system temporary directory is frequently on a different filesystem.
Replacement is attempted with rename first, because POSIX defines it as one atomic operation that
replaces an existing target. When it fails, the implementation removes the target and renames again.
ISO C leaves renaming onto an existing name undefined, and Windows documents it as a failure rather
than a replacement, so the fallback exists for a platform this release does not verify. It is not
atomic, because a process interrupted between the removal and the second rename leaves no target at
all. It therefore carries a TODO comment with that technical reason rather than a support claim.
The temporary file is removed only when this call created it, so a name that already belonged to something else is left where it was.
Consequences
A failed write leaves any previous payload intact, which is the property the tests assert directly.
A write needs space for two copies of the payload and a directory that accepts a second name. Both are true wherever the first copy could be written, at 84 bytes.
The window in which a target does not exist is a real difference between the two platforms. On POSIX there is no such window. On the fallback path there is, and a host that needs to close it on Windows has to use a platform replacement call, which is a change to this implementation rather than to the contract.
The temporary name is derived rather than made unique, so two processes writing the same target at once would collide. Concurrent access is outside the scope of this release, and a single desktop application saving its own pet does not meet it.
Alternatives considered
Writing directly into the target. Rejected because a failure destroys the previous payload, which is the one outcome this component exists to avoid.
A system temporary directory. Rejected because renaming out of it is a copy across filesystems in the common case, which is neither atomic nor a rename.
A unique temporary name. Rejected because it buys concurrency safety that the release does not claim, and it costs either a random source in a component that has none or a platform call the contract avoids.
Keeping a backup copy of the previous payload. Rejected because it doubles the number of files a host has to reason about and answers a question, recovering an older pet, that the product has not asked.
Replacement with a platform call such as ReplaceFile or renameat2. Rejected for this release
because the component would gain a platform header and a conditional path for a target with no test
evidence behind it. The TODO records where that work belongs.
References
- SPEC-FR-037, SPEC-NFR-004
- Architecture for v0.4.0, sections 5.3, 5.4, and 8.7
PBI-092,PBI-093