Undo Is Not an Architecture. It's the Log.
Undoing a rename is appending the old name. The value of a recovery mechanism is delivered continuously, to people who never press it.
Date
Category
Writer

When it came time to build undo, I did what most people do. I started sketching an architecture. A command stack. Inverse operations. Snapshots of prior state. A general mechanism that any action could register itself with.
Then I stopped, because I already had one and had not noticed.
The log was already the answer
JEFIELD is built on an append-only event log. Everything that has happened is in it, in order, immutably. Nothing is ever edited in place.
So: what is undoing a rename?
It is appending the old name.
Fields resolve last-write-wins in canonical order, which means this composes for free. Two devices that both rename and both pin the same thing converge correctly with zero merge code written for that case. Not because the merge was cleverly designed, but because the data model made the case impossible to express.
Command stack | Snapshots | The log | |
|---|---|---|---|
New concepts | Inverse of every action | Serialise whole state | None |
Can disagree with truth | Yes | Yes | Impossible |
Cost per new feature | Write an inverse | Grows the snapshot | Zero |
Survives a merge | Needs design | Needs design | Free |
History shows | One action | One action | Both actions |
Table 1. The last row is the only column where the log is arguably worse, and I think it is actually better.
The cost, stated rather than buried
There is a real cost and it belongs in the open.
The history now contains both renames. If you name something brief, then final brief, then undo, the log says: named brief, named final brief, named brief.
A snapshot-based undo would show one rename. The log shows three events.
And I think the log is right, because that is what actually happened. The person really did rename it twice. The second rename was real. They typed it, looked at it, and changed their mind. A system that erases the middle step is not being tidy. It is editing the past to produce a cleaner story than the one that occurred.
Undo should return you to a previous state. It should not pretend you were never anywhere else.
This has a practical consequence I did not anticipate. Because the log keeps the intermediate step, it is possible to ask questions about hesitation: which things got renamed more than once, which got renamed and then reverted. I am not building anything on top of that, and I have no plans to. But the data is there because the model did not throw it away, and that is generally how useful capabilities arrive. Not from a roadmap. From a representation that declined to discard something.
Why this is a design decision, not an engineering one
Apple's reintroduced design principles put recovery under Agency, and frame it in a way I have been thinking about since:
"When people know they can reverse an action or return to a previous state, they feel free to explore, and that freedom makes your interface more inviting. Build forgiveness into your design, and make it easy."
The value is not delivered when undo runs. It is delivered continuously, to people who never press it, in the form of permission.
Undo is what converts renaming something from a decision into an experiment. That is a change in the character of the interaction, not the availability of a feature, and it affects every user on every action whether or not the mechanism is ever invoked.
Which means the implementation cost has to be low enough that you build it early, before anyone asks for it. By the time someone asks, they have already spent months being careful, and the carefulness has already shaped what they did and did not try. You cannot recover that.
The bug that proves the approach
One failure worth reporting, because it was invisible in exactly the way undo bugs are.
Moving an item and then undoing the move did nothing. Silently. No error, no warning, no visual glitch. The undo appeared to run and the item stayed where it was.
The cause:
Fixed by representing it as an empty string, with a test that round-trips the value through JSON rather than asserting on it in memory.
The general shape is worth naming: a value that survives your object model but not your serialiser will fail only after a reload, which is precisely when nobody is watching. In-memory tests pass. Integration tests pass if they do not restart. The failure requires a session boundary, and session boundaries are the least-tested moment in most applications.
The rule I would keep
Before building a mechanism, ask whether your data model already contains the answer.
If you have an append-only record of what happened, undo is an append.
If you have immutable state, history is free.
If you have a deterministic reduction, time travel is a parameter.
If you have content-addressed storage, deduplication does not exist as a problem.
Most of the architecture I have not had to build on this project was avoided at the data layer, years before the feature was requested. The best mechanism is frequently the one you discover you already have, and the way you discover it is by describing the feature in terms of your existing model before you start designing a new one.
Undo is an append. That is not a clever trick. It is what happens when the representation is honest about time.
Sources
Apple. Design principles, Human Interface Guidelines. Reintroduced 8 June 2026. developer.apple.com/design/human-interface-guidelines/design-principles
Related reading
Latest Articles.
© Moc Mallen Studio
Thoughts, ideas, and perspectives on design, simplicity, and creative process.

