Day 13: You Can't Break It

undo compensating-operations time-travel replay audit

Most undo systems mutate history. They delete rows, revert fields, rewrite the past. Ours doesn't touch a single byte. Undo creates new operations that reverse effects. The past is immutable. The future is just more operations.

The Problem with Traditional Undo

Traditional applications implement undo by mutating state:

  • "Undo create" deletes the row
  • "Undo update" overwrites with previous values
  • "Undo delete" resurrects from a backup table

This works, until you try to sync. Two devices. Both undo. Which history is real? The append-only operation log solves this, but only if undo follows the rules.

Compensating Operations

Instead of mutating history, we create compensating operationsβ€”new operations that reverse the effects of previous ones:

// Traditional undo (mutates history)
DELETE FROM transactions WHERE id = 'tx_123'

// Compensating operation (preserves history)
transaction.revert β†’ Sets deletedAt, links to original operation

The original transaction.create operation still exists. The transaction.revert operation is a new entry that marks the transaction as deleted. Both operations are preserved.

What We Built

Undo Creates

transaction.revert soft-deletes the transaction by setting deletedAt. The row stays.

Undo Updates

transaction.revert.update restores previous field values. We track every changed field.

Undo Deletes

transaction.restore clears deletedAt. The row was never really gone.

Undo Transfers

transfer.revert soft-deletes both sides atomically. Transfer integrity preserved.

The Undo Operation Structure

Every undo operation carries a reference to what it undoes:

{
  "opType": "transaction.revert",
  "entityId": "tx_abc123",
  "payload": {
    "transactionId": "tx_abc123",
    "accountId": "acc_456",
    "originalOperationId": "op_789"  // ← Links to the operation being undone
  },
  "metadata": {
    "undoOf": "op_789",              // ← Also tracked in metadata
    "undoType": "transaction.create"
  }
}

This linking is crucial. It prevents double-undo, enables redo, and makes the audit trail complete.

Read-Only Time Travel

With an immutable operation log, time travel becomes trivial. Want to see your finances on December 1st?

// Time travel is read-only computation
const historicalState = getStateAt(workspaceId, dec1Timestamp);

// We replay operations up to that timestamp
operations.filter(op => op.timestamp <= dec1Timestamp)
          .forEach(op => applyOperationToState(state, op));

// Result: transactions and balances at that moment
historicalState.transactions  // What existed then
historicalState.balances      // Account balances at that point
historicalState.netWorth      // Net worth on Dec 1st

No snapshots stored. No separate backup tables. The operation log is the complete history. We just replay to any point.

How Replay Works

The operation replay service can reconstruct state from operations:

  • Chronological order β€” Operations replayed in timestamp order
  • Through LedgerService β€” Every replay enforces invariants
  • Idempotent β€” Replaying the same operation twice is safe
  • Deterministic β€” Same operations always produce same state

Undo + Sync = No Conflict

This is where compensating operations shine. Consider two devices:

// Device A
transaction.create   (op_1, ts: 100)
transaction.revert   (op_2, ts: 200)  // Undo

// Device B (offline, doesn't know about op_2)
transaction.update   (op_3, ts: 150)  // Updates the same tx

// After sync merge:
op_1: transaction.create  (ts: 100)
op_3: transaction.update  (ts: 150)  // Applied to existing tx
op_2: transaction.revert  (ts: 200)  // Soft-delete wins (later timestamp)

// Final state: transaction exists but is soft-deleted
// All operations preserved, chronologically correct

Traditional undo would have deleted the row. Device B's update would conflict. With compensating operations, everything merges cleanly because nothing is ever deletedβ€”only marked.

Safety Guarantees

GuaranteeHow We Enforce It
No double-undoCheck if operation already has an undo operation
Undo is auditableUndo operations link to their targets via metadata
Redo is possibleUndo operations themselves can be undone
Balances stay correctsyncAccountBalanceCache() after every undo

The Validation Step

Before any undo executes, we validate:

validateUndo(operationId)
β”œβ”€β”€ Does operation exist?
β”œβ”€β”€ Is operation type undoable?
β”œβ”€β”€ Has it already been undone?
β”œβ”€β”€ Does target entity exist?
└── Is entity in expected state?

Returns: { canUndo: boolean, reason: string }

Clear error messages. No silent failures. If undo can't proceed, you know exactly why.

Undo used to mean "rewrite history." Now it means "add to history." Every action, every undo, every redoβ€”all preserved, all auditable, all sync-safe.

You literally can't break your financial data. Every change is reversible. The past is forever.

β€” The Accelerate Finance Team