skip to content

How would you model a single user action that must perform several operations atomically as command objects, and what happens if one of them fails partway through?

level: middleimportance: should knowfreq 30%

answer

  1. Composite + Command = macro command
  2. same interface → history sees one entry
  3. undo children in REVERSE order
  4. roll back only the executed prefix
  5. irreversible last; DB txn vs saga

basics

~20 s

Wrap the child commands in one macro (composite) command that implements the same interface. Its execute() runs children in order; if one fails, it undoes the already-executed children in reverse order and rethrows, so the history sees one entry that either fully applied or not at all.

solid answer

~60 s

Use a composite/macro command: an object implementing the same `Command` interface that holds an ordered list of child commands. `execute()` iterates forward; `undo()` iterates **backward**, because later commands may depend on the state established by earlier ones. Because it satisfies the same interface, the invoker and the undo history treat it as a single unit — which is what the user expects, since one gesture should be one undo step. Failure handling is the interesting part: on an exception at child *k*, undo children 0..k-1 in reverse and propagate the error, so the macro never lands half-applied on the history stack. That gives all-or-nothing semantics *if* each child is reliably reversible; when a child touches an external system that cannot be reversed, you get compensation semantics (a saga), not a transaction — order the irreversible steps last, or move them outside the macro. If all children hit one transactional resource, the simpler answer is to let the database transaction provide atomicity and keep the macro purely for undo granularity.

code

typescript · 17 lines
typescript
class MacroCommand implements Command {
  private executed: Command[] = []
  constructor(private children: Command[], readonly label: string) {}

  execute() {
    for (const c of this.children) {
      try { c.execute(); this.executed.push(c) }
      catch (e) { this.rollback(); throw e }   // never leave it half-applied
    }
  }
  undo() { this.rollback() }

  private rollback() {
    for (let i = this.executed.length - 1; i >= 0; i--) this.executed[i].undo()
    this.executed = []
  }
}

go deeper

for a junior

Say you wrap the operations in one command holding a list, run them in order, and undo them in reverse so the user sees a single undo step.

for a middle

Add the failure path: track which children ran, roll back only that prefix, rethrow — and note the composite implements the same interface so the invoker cannot tell the difference.

for a senior

Distinguish true transactional atomicity from compensation, discuss irreversible side effects and ordering, validate-then-apply, nesting, and macro vs coalescing.

for a principal

Frame the multi-resource case as a saga with retryable idempotent compensations and an escalation path, discuss observability of intermediate states, and decide deliberately where the consistency boundary sits rather than emulating transactions in application code.

## The requirement "Group selected shapes" might mean: create a group node, reparent five shapes, recompute the bounding box, and mark the document dirty. The user did **one** thing. If the undo stack shows four entries, undo feels broken. And if step 3 fails, the document must not be left with a group node containing two of five shapes. ## Composite command Apply Composite to Command: a macro command implements the same interface and delegates to an ordered list of children. ``` class MacroCommand implements Command { children = [...] executed = [] execute() { for (c in children) { try { c.execute(); executed.push(c) } catch (e) { rollbackExecuted(); throw e } } } undo() { for (c in reverse(executed)) c.undo(); executed.clear() } rollbackExecuted() { for (c in reverse(executed)) c.undo(); executed.clear() } } ``` Three points an interviewer listens for: 1. **Same interface** — the invoker, scheduler, and history cannot tell a macro from a leaf. That uniformity is the whole benefit of Composite. 2. **Reverse order on undo** — later children were executed against the state produced by earlier ones. Undoing forward would try to reverse operations whose preconditions no longer hold (delete a node before removing its children, restore a value that a later command overwrote). 3. **Track what actually executed** — roll back only the prefix that ran, never the child that threw (it either fully failed or already rolled itself back) and never the ones never reached. ## Atomicity: what you actually get Be precise here, because "atomic" is doing a lot of work: - **True atomicity** when all children mutate a single transactional resource — then wrap the macro in one database transaction and let the engine roll back. The macro still earns its place as the *undo granularity* unit, not as the atomicity mechanism. - **Compensation-based atomicity (saga semantics)** when children touch multiple systems. You are not rolling back; you are applying inverse operations after the fact. Between the failure and the compensation, other observers can see the intermediate state — this is not isolation, only eventual consistency. Compensations can themselves fail, so they must be retryable and idempotent, and you need an escalation path (alert / manual repair) when they don't. - **No atomicity at all** for irreversible steps (email sent, payment captured, file deleted from an external store). The mitigations: order irreversible steps **last**, so the reversible work is validated first; or split them out of the macro entirely and trigger them only after the reversible part commits (outbox-style). ## Validate-then-apply A large fraction of partial-failure pain disappears if the macro validates everything up front: run all preconditions and computations that can fail *before* mutating anything, so `execute()` is effectively a sequence of operations that cannot throw for foreseeable reasons. This is the same discipline as making a single command's `execute()` atomic. ## Nesting and other refinements - **Nesting**: a macro may contain macros; rollback recurses naturally. - **Empty macro**: executing zero children should be a no-op and generally should not be pushed onto the history. - **Coalescing vs macro**: merging 200 keystrokes into one entry is *coalescing* (dynamic, after the fact); a macro is a *statically composed* unit built at creation. Both fix undo granularity; don't conflate them. - **Describing the macro**: the undo menu label should describe the user intent ("Undo Group Shapes"), not the children. - **Parallel children**: if children are independent you may run them concurrently, but then "reverse order" loses meaning and you need per-child compensation with no ordering assumption — usually a sign the operation isn't really one atomic action. ## When not to use a macro If the children are only ever used together and never independently, a single command with a cohesive `execute()`/`undo()` pair may be simpler than a composite of four one-line commands. Reach for the macro when the children genuinely exist on their own (they are also invoked separately, or are recorded/scripted), or when the sequence is built dynamically at runtime — for example a recorded macro, a batch import, or a scripted automation.

  • Why must a composite command undo its children in reverse order?
    Later children executed against the state created by earlier ones. Undoing forward would attempt reversals whose preconditions have been invalidated — for example removing a parent node before the children added to it, or restoring a value that a later command has since overwritten. Reverse order restores each precondition just before it is needed.
  • One child sends a confirmation email and a later child fails. What now?
    The email cannot be undone, so the group is not atomic. Either reorder so irreversible effects run last, after everything that can fail has succeeded, or take the effect out of the macro and trigger it only once the reversible part has committed (outbox). If it must stay, the compensation is an explicit follow-up action (a correction email), not a rollback.
  • When would you use a database transaction instead of hand-rolled rollback in the macro?
    Whenever every child mutates the same transactional store: the engine gives real atomicity and isolation, which compensation cannot. The macro then exists only to give the user one undo entry and to describe the intent.

A surgical checklist executed in order: to abort halfway you retrace the steps you already performed, newest first — you cannot close the incision before removing the instruments you inserted afterwards.

saying these in an interview costs you the question

  • Undoing children in the same order they were executed.
  • Rolling back children that never ran, or attempting to undo the child that threw.
  • Pushing each child onto the undo stack separately, so one user action needs four undos.
  • Calling compensation-based rollback across multiple systems 'atomic' — it has no isolation and intermediate states are observable.
  • Placing an irreversible external effect early in the sequence, before the steps that can still fail.
  • Not giving the macro its own user-facing description, so the undo menu shows an internal child operation.
  • Building a composite when a single cohesive command would do, purely to look pattern-compliant.

context