← Build log

From Revert to Recovery: How Simplicity Won the Day

January 11, 2025
SoloBuilderAutoRecoverySimplicityUXDesignShippingWinsPragmaticEngineering

After reverting a “perfect” architectural fix, we shipped a working auto-recovery feature by embracing simplicity and invisible UX

Published: January 11, 2025
Tags: #SoloBuilder #AutoRecovery #Simplicity #UXDesign #ShippingWins #PragmaticEngineering


The “So What?” - Victory Through Simplification

Bottom Line Value: Learn how pivoting from a failed refactor to a simpler approach—combined with ruthless UX simplification—delivered a production-ready crash recovery feature in hours.

What You’ll Gain:

  • How to recover momentum after a major revert
  • The power of “always-on” vs. “configurable” features
  • Why invisible UX is the best UX for safety features
  • Real example: From broken architecture to shipped feature
  • Framework for simplifying features that users shouldn’t think about
  • ROI: Ship features faster by removing unnecessary complexity

Who This Helps: Solo builders recovering from failed refactors, engineers over-engineering features, product builders struggling with feature complexity.

Why Bother: The fastest path to shipping isn’t always the most elegant architecture. Sometimes the best feature is the one users never notice—until it saves their work.


TLDR: From Failure to Feature in One Day

Where we left off (Blog #130):

  • ❌ Spent 6+ hours trying to eliminate dual audio path
  • ❌ “Perfect” single-path architecture completely broke recording
  • ✅ Made hard decision to revert everything
  • ✅ Dual-path architecture restored, recording works perfectly

What happened next:

  • ✅ Discovered checkpointing was never working (design flaw)
  • ✅ Simplified: Made checkpointing always-on (removed all configuration)
  • ✅ Tested: Force-quit via Activity Monitor (real crash simulation)
  • ✅ Auto-recovery works perfectly end-to-end
  • ✅ Feature shipped: Invisible, automatic, production-ready

The lesson: After a failed refactor, the path forward is often simpler than you think.


The Discovery: It Never Worked

The Moment of Truth

After reverting the dual-path refactor, we started testing the auto-recovery feature. The plan was simple:

  1. Start recording
  2. Speak for 90 seconds
  3. Check for checkpoint files

Expected result: Checkpoint files every 30 seconds
Actual result: No checkpoint files found

Wait, what?

The Root Cause: Optional by Default

The checkpoint system was designed to be “always-on” (per the design docs), but implemented as “optional”:

// The problem: Defaults to false
private var checkpointingEnabled: Bool = false

// Requires explicit configuration
func enableCheckpointing(sessionId: String, interval: TimeInterval) {
    self.checkpointingEnabled = true
    // ...
}

The TypeScript side:

// Requires passing parameters
checkpointing: {
  enabled: true,  // Could be false!
  sessionId: generateSessionId(),
  interval: 30
}

What went wrong:

  • If parameters weren’t passed correctly → checkpointing disabled
  • If there was a bug in parameter passing → checkpointing disabled
  • Default state → checkpointing disabled

The feature was designed to always work, but implemented to usually fail.


The Pivot: Always-On Architecture

User’s Insight

“I want this feature always on. I’m not clear on why it needs to be initialized to false at all, or even parameterized. It should just be core to how we record.”

This changed everything.

The Simplification

Before (Optional):

// Swift side
private var checkpointingEnabled: Bool = false
func enableCheckpointing(sessionId: String, interval: TimeInterval)
func disableCheckpointing()

// Command-line parameters
--checkpoint-interval 30 --session-id 1234567890-123456

After (Always-On):

// Swift side
private let checkpointInterval: TimeInterval = 30.0  // Hardcoded
private var currentSessionId: String = ""  // Auto-generated

// No command-line parameters needed
// No enable/disable methods
// Just works

What we removed:

  • ❌ checkpointingEnabled flag
  • ❌ enableCheckpointing() method
  • ❌ disableCheckpointing() method
  • ❌ Command-line parameters --checkpoint-interval and --session-id
  • ❌ TypeScript parameter building code
  • ❌ Session ID generation in TypeScript
  • ❌ Entire checkpointing configuration object

What we gained:

  • ✅ Simpler code (less code = fewer bugs)
  • ✅ Impossible to disable (can’t forget to enable it)
  • ✅ No configuration needed (zero user-facing settings)
  • ✅ Actually works (tested with real crash)

The UX Philosophy: Invisible Safety

The Design Constraint

From AUTO_RECOVERY_SUMMARY.md:

⚠️ CRITICAL UX CONSTRAINT: INVISIBLE TO NORMAL USERS

Auto-recovery must be COMPLETELY INVISIBLE unless a crash occurs.

This isn’t just a nice-to-have—it’s the entire point.

Why Invisible UX Matters

Bad UX (Visible Safety):

┌─────────────────────────────────────┐
│ ⚙️ Settings                         │
├─────────────────────────────────────┤
│ ☑ Enable crash recovery            │
│ Checkpoint interval: [30] seconds   │
│ Session ID format: [timestamp-rand] │
│ Cleanup policy: [After recovery]    │
└─────────────────────────────────────┘

Problems:

  • Users have to understand what “checkpointing” means
  • Users might disable it (thinking it slows things down)
  • Users have to configure intervals (what’s the right value?)
  • More settings = more support questions
  • Most importantly: Users shouldn’t have to think about crashes

Good UX (Invisible Safety):

┌─────────────────────────────────────┐
│ Recording...                        │
│ ⏺ 00:45                             │
└─────────────────────────────────────┘

[Checkpoint files being written silently in background]
[User has no idea this is happening]
[User doesn't need to know]

If the app crashes:

┌─────────────────────────────────────┐
│ 🔄 Recovered Session                │
├─────────────────────────────────────┤
│ We recovered your recording from    │
│ before the crash (2m 30s).          │
│                                     │
│ [View in History]  [Dismiss]        │
└─────────────────────────────────────┘

Benefits:

  • ✅ User only sees recovery when it matters (after a crash)
  • ✅ No configuration needed (zero cognitive load)
  • ✅ No settings to explain (zero documentation needed)
  • ✅ No way to disable it (zero support questions)
  • ✅ Just works (zero user effort)

The Test: Real Crash Simulation

The Setup

Step 1: Start recording

  • Press CTRL+SHIFT+` (manual recording mode)
  • Speak continuously for 90+ seconds

Step 2: Verify checkpoint files

ls -lh /var/folders/.../T/recording_session-*_checkpoint-*.wav

Result:

-rw-r--r--  1 user  staff   1.1M Jan 11 14:23 recording_session-1762822825-196880_checkpoint-000.wav
-rw-r--r--  1 user  staff   1.1M Jan 11 14:23 recording_session-1762822825-196880_checkpoint-001.wav
-rw-r--r--  1 user  staff   1.1M Jan 11 14:24 recording_session-1762822825-196880_checkpoint-002.wav

✅ Checkpoint files being created at 30-second intervals

Step 3: Force-quit via Activity Monitor

  • Open Activity Monitor
  • Find “Electron” process
  • Click Force Quit (X button)
  • Confirm

This simulates a real crash (immediate termination, no cleanup)

Step 4: Restart VoiceCatcher

What happened:

  1. ✅ App detected checkpoint files on startup
  2. ✅ Automatically assembled audio from checkpoint files
  3. ✅ Automatically transcribed using Whisper
  4. ✅ Automatically saved to history
  5. ✅ Recovery notification appeared
  6. ✅ “View in History” button opened history window
  7. ✅ Recovered session visible with correct transcription
  8. ✅ Checkpoint files cleaned up after recovery

End-to-end auto-recovery: WORKING PERFECTLY 🎉


The Lessons: Simplicity Wins

1. Always-On > Configurable (For Safety Features)

When to make features always-on:

  • Safety features (crash recovery, auto-save, backups)
  • Features users shouldn’t think about
  • Features that have no downside to being enabled
  • Features where “forgetting to enable” = data loss

When to make features configurable:

  • Features with performance trade-offs
  • Features with different user preferences
  • Features that change behavior users care about
  • Features where users need control

Auto-recovery is clearly in the first category.

2. Invisible UX > Visible UX (For Background Features)

The best UX is no UX:

  • Users don’t configure it
  • Users don’t see it (until they need it)
  • Users don’t think about it
  • Users just trust it works

This is how macOS Time Machine works:

  • No configuration needed (just enable once)
  • Runs silently in background
  • Only visible when you need to restore
  • Users trust their data is safe

3. Simplification After Failure

After reverting the dual-path refactor, we could have:

  • ❌ Tried another architectural approach
  • ❌ Spent more time debugging the single-path solution
  • ❌ Blamed the tools (Swift, AVFoundation, etc.)

Instead, we:

  • ✅ Tested the existing feature
  • ✅ Found it didn’t work (design flaw)
  • ✅ Simplified the design (always-on)
  • ✅ Shipped a working feature

The revert freed us to focus on what matters: shipping.

4. Test with Real Scenarios

We didn’t test with:

  • ❌ Simulated crashes in code
  • ❌ Graceful shutdown with cleanup
  • ❌ Unit tests mocking the crash

We tested with:

  • ✅ Force-quit via Activity Monitor
  • ✅ Real checkpoint files in temp directory
  • ✅ Real transcription with Whisper
  • ✅ Real recovery notification
  • ✅ Real user flow end-to-end

This is the only way to know it actually works.


The Framework: Simplify, Test, Ship

Step 1: Identify Unnecessary Complexity

Questions to ask:

  • Does this need to be configurable?
  • Does the user need to know about this?
  • What happens if we remove this parameter?
  • Can we hardcode this value?
  • Is this feature optional or essential?

For auto-recovery:

  • ❌ Checkpoint interval doesn’t need to be configurable (30s is fine)
  • ❌ Session ID doesn’t need to be passed from TypeScript (generate in Swift)
  • ❌ Enable/disable doesn’t make sense (always enable)
  • ✅ Remove all of it

Step 2: Make It Always-On

Code changes:

- private var checkpointingEnabled: Bool = false
+ private let checkpointInterval: TimeInterval = 30.0

- func enableCheckpointing(sessionId: String, interval: TimeInterval) {
-   self.checkpointingEnabled = true
-   self.currentSessionId = sessionId
-   self.checkpointInterval = interval
- }
+ // Checkpointing is always enabled, no method needed

- if checkpointingEnabled {
-   saveCheckpoint()
- }
+ saveCheckpoint()  // Always save

Result: Feature can’t be disabled, can’t be misconfigured, can’t fail to initialize.

Step 3: Test with Real Crashes

Don’t simulate—actually crash:

  1. Start the feature
  2. Force-quit the app (Activity Monitor, kill -9, etc.)
  3. Restart the app
  4. Verify recovery works end-to-end

If it doesn’t work, you’ll know immediately.

Step 4: Ship It

Once it works:

  • ✅ Document the feature (user guide)
  • ✅ Update help system
  • ✅ Write blog post (you’re reading it!)
  • ✅ Ship to users

Don’t wait for perfection—ship working features.


The Metrics: What We Shipped

Code Removed (Simplification)

Swift:

  • Removed checkpointingEnabled flag
  • Removed enableCheckpointing() method
  • Removed disableCheckpointing() method
  • Removed command-line parameter parsing for checkpoints
  • ~50 lines of code deleted

TypeScript:

  • Removed checkpointing parameter from RecordAudioOptions
  • Removed checkpoint parameter building in buildArgs()
  • Removed generateSessionId() from recording-controller.ts
  • Removed checkpoint config from recordAudio() calls
  • ~40 lines of code deleted

Total: ~90 lines of code removed ✂️

Features Shipped (Value)

Auto-Recovery:

  • ✅ Checkpoint files created every 30 seconds during recording
  • ✅ Checkpoint files survive crashes (stored in system temp)
  • ✅ Automatic detection on app restart
  • ✅ Automatic audio assembly (GPL-free, pure Node.js)
  • ✅ Automatic transcription (Whisper.cpp)
  • ✅ Automatic save to history
  • ✅ Recovery notification with “View in History” button
  • ✅ Checkpoint cleanup after successful recovery
  • ✅ Completely invisible to users (unless crash occurs)

User Impact:

  • 🎯 Zero data loss from crashes
  • 🎯 Zero configuration needed
  • 🎯 Zero cognitive load (users don’t think about it)
  • 🎯 100% automatic (no user action required)

The Takeaway: Pivot, Simplify, Ship

What We Learned

From the revert (Blog #130):

  • Know when to abandon a failing approach
  • Reverting is not failure—it’s pragmatism
  • Working code > elegant code

From the recovery (this post):

  • Simplify ruthlessly (always-on > configurable)
  • Invisible UX is the best UX (for safety features)
  • Test with real scenarios (force-quit, not mocks)
  • Ship working features (don’t wait for perfect)

The Pattern

Failed Refactor
    ↓
Revert to Working State
    ↓
Test Existing Features
    ↓
Find Design Flaws
    ↓
Simplify Ruthlessly
    ↓
Test with Real Scenarios
    ↓
Ship Working Feature

This is how solo builders ship: Pivot fast, simplify hard, test real, ship working.


Next Steps

What’s shipping next:

  • 📝 Update user documentation (recovery feature)
  • 📝 Update help system (crash recovery section)
  • 🧪 Add automated tests (checkpoint creation, recovery flow)
  • 🚀 Ship v0.4.1 with auto-recovery

What we’re NOT doing:

  • ❌ Trying to eliminate dual audio path again
  • ❌ Adding checkpoint configuration settings
  • ❌ Making recovery “smarter” (it works, ship it)

The focus: Ship features that work, not features that are perfect.


Resources


The Bottom Line: After reverting a failed refactor, we shipped a production-ready auto-recovery feature by making it always-on, invisible, and simple. Sometimes the best path forward is the one with the least code.

Ship working features. Simplify ruthlessly. Make UX invisible.


Written by a solo builder who learned that the best code is often the code you delete.