skip to content

In a two-stack undo/redo design, why must a new user action clear the redo stack?

level: seniorimportance: should knowfreq 45%

answer

  1. Ask what a redo entry silently assumes
  2. The user has just changed the past
  3. History has branched at this point
  4. Two linear stacks cannot hold a branch
  5. Replaying it would target objects that vanished

basics

~20 s

Redo entries are only meaningful against the exact state they were undone from. A new action rewrites history from that point, so replaying them would target objects that no longer exist. Clearing the redo stack keeps the model honest.

solid answer

~50 s

The two stacks encode one linear history. Undo pops the most recent action off the undo stack, applies its inverse, and pushes it onto the redo stack; redo pops it back, re-applies it, and returns it to the undo stack. Each redo entry therefore carries an implicit precondition: the document is in exactly the state that entry was undone from. When the user performs a **new** action from a mid-history position, the timeline branches, and the entries sitting in the redo stack now describe a future that no longer exists — their target indices, references and identifiers may not even resolve. Two linear stacks cannot represent a branch, so the discipline is to discard: push the new action onto the undo stack and empty the redo stack. Browser-style back/forward is the identical invariant — navigating somewhere new from a back position drops the forward entries.

go deeper

for a junior

Be ready to describe the flow of entries: undo moves the top action to the redo side, redo moves it back, and a fresh action goes onto the undo side. Remember that a new action wipes the redo side.

for a middle

Explain the invariant behind the rule — a redo entry is only valid against the state it was undone from — and why two linear stacks cannot represent the branch a new edit creates.

for a senior

Show the production consequences: the stale-reference bug and why it barely reproduces, snapshots versus invertible commands, coalescing edits into user-meaningful units, and which interactions belong in history at all.

for a principal

Own the ceiling and the alternative. Decide how the history is bounded on real documents, when a branch-preserving history tree is worth its memory and its interface, and what maintaining a correct inverse per command type costs the team over time.

## The model Undo/redo in a vector-graphics editor is the textbook two-stack pair, and it is worth stating the whole contract before the failure mode, because the failure mode is a consequence of it. The editor keeps a linear history of user actions. Two stacks divide it at the current position: - the **undo stack** holds everything already applied, most recent on top; - the **redo stack** holds everything that has been undone and could be re-applied, most recently undone on top. Three operations move entries between them: 1. **New action** — apply it, push it onto the undo stack, and clear the redo stack. 2. **Undo** — pop from the undo stack, apply the inverse, push the entry onto the redo stack. 3. **Redo** — pop from the redo stack, re-apply the entry, push it back onto the undo stack. Step 1's second half is the rule people forget, and it is the whole question. ## Why clearing is forced, not stylistic Every redo entry carries an unwritten precondition: *the document is in exactly the state this entry was undone from*. That is what makes re-applying it meaningful. Undo and redo are inverses only along one timeline. Suppose an illustrator draws a rectangle, applies a gradient to it, then undoes twice. The redo stack now holds "apply gradient" and above it nothing else; the canvas is empty. Instead of redoing, the illustrator draws a circle. The timeline has just **branched**: the past everyone agreed on ends at the empty canvas, and history now continues down a different path. The entry "apply gradient to the rectangle" still sits in the redo stack, but the rectangle it refers to no longer exists. If the redo entry is kept and later replayed, one of three things happens, in increasing order of nastiness: it fails cleanly because the target cannot be resolved; it does nothing and leaves the user's redo command silently inert; or — worst and most common — the identifier it stored has been reused by the new object, and the gradient lands on the circle. The user sees a change they never made, with no action of theirs to blame. This class of bug is hard to reproduce from a report because it requires the specific undo-then-diverge sequence. The deeper reason is representational. Two linear stacks can express exactly one sequence. A branch needs a tree, and if you keep the abandoned entries you have a data structure making a promise the model cannot keep. Clearing is the model telling the truth. ## The same shape elsewhere Back/forward navigation is this design under a different name: a back stack of visited locations and a forward stack of locations stepped back past. Going back moves an entry from one to the other; going forward moves it back. Following a **new** link while sitting several steps back empties the forward stack for exactly the reason above — the future you had stepped out of is no longer the future you are in. Users have internalised this so thoroughly that a product violating it feels broken, even though most could not state the rule. ## What entries should hold The stacks store actions, and there are two families: - **Snapshots** — a copy of the affected state before and after. Trivially correct, trivially reversible, and expensive: on a large document the undo stack becomes the biggest object in the process. Structural sharing between snapshots reclaims much of that. - **Commands** — a description of the change plus enough information to invert it ("moved these three shapes by this offset", "set fill from old value to new"). Compact, but every command type needs a correct inverse, and one wrong inverse corrupts the document in a way that only shows up several undos later. Most real editors mix them: commands for ordinary edits, a snapshot checkpoint for operations that are hard to invert, such as a filter over pixels. ## The judgment layer Three decisions come up in any real implementation and are worth raising unprompted. - **Granularity and coalescing.** One keystroke per undo entry is unusable; users expect a word, a drag gesture, or a burst of edits within a short window to undo as a unit. Coalescing happens when the entry is pushed, and it is a product decision as much as a technical one. - **Bounding.** An unbounded undo stack is an unbounded memory commitment. Capping depth, or capping total bytes and evicting the oldest entries from the bottom, keeps the ceiling predictable — note that this eviction is from the *bottom*, which a plain stack interface does not offer, so the underlying container usually needs access at both ends. - **Scope.** Not everything belongs in history. Selection changes, scroll position and view zoom generally should not be undoable, while document mutations must be. Getting this wrong produces the common complaint that undo "does nothing" — it undid a selection. ## Beyond two stacks If branch history genuinely matters, the honest answer is a different structure: a history tree or a persistent version graph where an undo followed by a new action creates a sibling branch rather than discarding one. It is strictly more powerful and strictly more expensive — in memory, in the interface needed to let a user navigate branches, and in the team's ongoing cost of maintaining it. The reason two stacks dominate is that the linear model matches what users already expect, and the branch they lost was one they had explicitly stepped away from.

  • What symptom would a team see if the redo stack were never cleared?
    A redo command that applies a change nobody made. The stale entry references an object that no longer exists, so it either fails, does nothing, or — when identifiers have been reused — lands on whatever new object took the old one's place. It only reproduces after an undo followed by a divergent edit, so bug reports rarely include the sequence that triggers it.
  • Would you store snapshots or invertible commands on the undo stack?
    Commands for ordinary edits, because they are compact and carry their own inverse; snapshots as checkpoints for operations that are impractical to invert, such as a filter over pixel data. The tradeoff is memory against correctness risk: snapshots are trivially right but expensive, while every command type is one more inverse that can be wrong, and a wrong inverse corrupts the document several undos later.
  • How do you keep an undo history from growing without bound on a long editing session?
    Cap it — by entry count, or better by total bytes, evicting the oldest entries from the bottom of the history. Note that evicting from the bottom is not a stack operation, so the underlying container needs access at both ends even though the semantics stay LIFO. Structural sharing between snapshots and coalescing bursts of small edits both cut the pressure before the cap is reached.
  • How would you support keeping the abandoned branch instead of discarding it?
    Replace the two stacks with a history tree, where an edit made after an undo creates a sibling branch rather than clearing anything. It is strictly more powerful, and strictly more expensive: more memory, a user interface for navigating branches that most people never want, and a permanent maintenance cost. Two stacks win because they match the linear model users already expect.

The redo stack is like the pages you flipped back past in a notebook. The moment you start writing on the page you flipped back to, the pages ahead describe a story that no longer follows, so you tear them out rather than leave them to confuse the next reader.

saying these in an interview costs you the question

  • Keeps redo entries after a new edit so nothing is lost
  • Cannot say what precondition a redo entry assumes
  • Treats undo and redo as one stack toggling direction
  • Assumes every operation has an obvious inverse
  • Leaves the undo history unbounded in memory
  • Puts selection and scroll changes into the history

context