When Tech Debt Blocks Features: The Dual Audio Path That Almost Killed Auto-Recovery
How a “temporary” architectural compromise became a critical blocker—and why we’re fixing it before v1.0 launch
Published: January 10, 2025 Updated: January 11, 2025 Status: ⚠️ POSTMORTEM - Plan Changed (See update below) Tags: #TechDebt #ArchitecturalDecisions #RiskManagement #SoloBuilder #PreLaunchDecisions #AudioEngineering
⚠️ UPDATE (January 11, 2025): When the Fix Becomes the Problem
TL;DR: We implemented the single-path architecture described in this post. It broke recording completely. After hours of debugging, we reverted to the dual-path architecture. Sometimes the best decision is to fold.
What happened:
- ✅ Implemented single-path architecture (AVAudioEngine only)
- ❌ Recording stop functionality completely broken
- ❌ Swift process hung, didn’t respond to SIGINT
- ❌ Multiple audio graph configurations all failed
- ✅ Reverted to dual-path architecture
- ✅ Recording works perfectly again
The lesson: Even well-analyzed, well-planned architectural changes can fail in practice. Knowing when to revert is as important as knowing when to refactor.
Read the full story: Blog Post #130: Know When to Hold ‘Em, Know When to Fold ‘Em
Current status: Dual-path architecture restored and working. Auto-recovery feature shipped with GPL-free WAV concatenation instead of SoX.
The “So What?” - When to Fix Tech Debt vs. Ship Around It
Bottom Line Value: Learn how to identify when tech debt has crossed from “annoying” to “blocking” and make confident decisions about fixing it before launch vs. shipping with workarounds.
What You’ll Gain:
- Framework for evaluating tech debt severity and impact
- Real example: Dual audio path causing checkpoint recovery failures
- Risk/reward analysis for pre-launch architectural fixes
- Decision criteria: Fix now vs. ship and fix later
- How to recognize “critical mass moments” in tech debt
- ROI: Avoid shipping features that don’t work due to underlying architectural issues
Who This Helps: Solo builders and TPMs who struggle with the “fix tech debt vs. ship features” dilemma, especially in pre-launch phases.
Why Bother: Shipping a broken feature is worse than delaying launch. This framework helps you identify when tech debt has become a launch blocker and make confident decisions about fixing it.
TLDR: The Checkpoint Files Were Silent
We built a complete auto-recovery feature for crash protection. It worked perfectly in testing. Then we tested with real recordings and discovered:
- ✅ Checkpoint files were being created every 30 seconds
- ✅ Recovery UI appeared after crashes
- ✅ Files were assembled and transcribed
- ❌ The transcripts were complete gibberish
Root cause: A dual audio path architecture where two systems competed for microphone access. One got perfect audio, the other got silence.
The decision: Fix the underlying architecture before v1.0 launch, even though it means rewriting core recording code.
Why now: Because shipping a “crash recovery” feature that recovers garbage is worse than not having the feature at all.
The Problem: When “Temporary” Becomes Permanent
The Original Architecture Decision
When we built VoiceCatcher’s Swift audio engine, we made a pragmatic choice:
Use AVAudioRecorder for the main recording:
- Simple, battle-tested API
- Just 12 lines of code
- Handles all file I/O automatically
- Reliable and well-documented
Add AVAudioEngine later for real-time features:
- Needed for silence detection (RMS calculation)
- Needed for amplitude monitoring
- More complex but more flexible
The assumption: “Both can use the same microphone input without conflict.”
The TODO Comment That Told the Truth
Buried in src/recorder.ts, we found this:
// TEMPORARY FIX: Disable silence detection due to AVAudioEngine/AVAudioRecorder conflict
// The Swift CLI has two competing audio systems that conflict with each other
// TODO: Fix Swift CLI to use single audio system for both recording and monitoring
Translation: We knew this was a problem. We shipped it anyway. We planned to fix it “later.”
When “Later” Became “Now”
Fast forward to implementing auto-recovery. The feature requires:
- Checkpoint files - Audio saved every 30 seconds during recording
- Recovery on crash - Assemble checkpoints and transcribe
- Accurate transcripts - Users need to know what they said
What we got:
User recording: "We need to discuss the Q4 budget and marketing strategy..."
Recovered transcript: "Oh, shh, shh, shh, shh, shh, shh, shh."
184 seconds of audio. 38 characters of hallucinated text.
The checkpoint files contained mostly silence because AVAudioEngine’s tap was getting starved by AVAudioRecorder’s exclusive microphone access.
The Critical Mass Moment: When Tech Debt Becomes a Blocker
The Evaluation Framework
We use this framework to decide “fix now vs. ship around it”:
1. Impact Severity
Question: How badly does this affect the feature?
- 🟢 Minor - Feature works with small quirks
- 🟡 Moderate - Feature works but with noticeable issues
- 🔴 Critical - Feature fundamentally broken
Our case: 🔴 CRITICAL - Auto-recovery recovers garbage
2. Workaround Feasibility
Question: Can we ship with a workaround?
- 🟢 Easy - Simple workaround, minimal user impact
- 🟡 Possible - Workaround exists but adds complexity
- 🔴 Impossible - No viable workaround
Our case: 🔴 IMPOSSIBLE - Can’t fix silent audio with software
3. User Trust Impact
Question: What happens if users encounter this?
- 🟢 Low - Minor annoyance, users understand
- 🟡 Medium - Frustrating but not trust-breaking
- 🔴 High - Destroys trust in the product
Our case: 🔴 HIGH - “Crash recovery” that doesn’t work = broken promise
4. Fix Complexity
Question: How hard is it to fix properly?
- 🟢 Simple - Few hours, low risk
- 🟡 Moderate - Few days, manageable risk
- 🔴 Complex - Weeks, high risk
Our case: 🟡 MODERATE - Rewrite recording path, but we have all the pieces
5. Launch Timeline Impact
Question: Does fixing this delay launch?
- 🟢 No delay - Can fix within current timeline
- 🟡 Minor delay - 1-2 weeks
- 🔴 Major delay - Months
Our case: 🟢 NO DELAY - We’re pre-launch, this IS the timeline
The Decision Matrix
| Factor | Score | Weight | Weighted Score |
|---|---|---|---|
| Impact Severity | 🔴 Critical | 5x | 15 |
| Workaround Feasibility | 🔴 Impossible | 4x | 12 |
| User Trust Impact | 🔴 High | 5x | 15 |
| Fix Complexity | 🟡 Moderate | 2x | 4 |
| Launch Timeline Impact | 🟢 No delay | 3x | 0 |
| TOTAL | 46/60 |
Threshold for “fix before launch”: 35/60
Our score: 46/60 → FIX NOW
The Risk/Reward Analysis
Risks of Fixing Now
1. Implementation Bugs
- Risk: New recording path could have bugs
- Mitigation: Comprehensive testing, reuse existing functions
- Probability: Medium
- Impact: High
- Overall: 🟡 Manageable
2. Timeline Slip
- Risk: Could delay v1.0 launch
- Mitigation: We’re already pre-launch, this is planned work
- Probability: Low
- Impact: Medium
- Overall: 🟢 Low
3. Breaking Existing Functionality
- Risk: Could break working recordings
- Mitigation: Extensive testing before merge
- Probability: Low (we have all the infrastructure)
- Impact: High
- Overall: 🟡 Manageable
Risks of Shipping Without Fix
1. Broken Feature
- Risk: Auto-recovery doesn’t work
- Probability: 100% (already confirmed)
- Impact: Critical
- Overall: 🔴 UNACCEPTABLE
2. User Trust Damage
- Risk: Users lose trust in “crash recovery”
- Probability: High
- Impact: Critical (trust is hard to rebuild)
- Overall: 🔴 UNACCEPTABLE
3. Support Burden
- Risk: Users report “recovery doesn’t work”
- Probability: High
- Impact: High (support time, reputation damage)
- Overall: 🔴 UNACCEPTABLE
4. Technical Debt Compounds
- Risk: Have to fix it later anyway, but with users affected
- Probability: 100%
- Impact: Critical (harder to fix with users)
- Overall: 🔴 UNACCEPTABLE
The Math
Fix now:
- 3-5 days of work
- Manageable risks with clear mitigation
- Ships with working auto-recovery
- Clean architecture for future features
Ship broken:
- 0 days now
- 100% chance of user-facing failures
- Have to fix later anyway (with users affected)
- Compounds tech debt
Decision: Fix now is the only viable option.
The Solution: Single-Path Architecture
What We’re Changing
Current (Dual-Path):
Microphone
├─> AVAudioRecorder ──> Main recording (perfect audio)
└─> AVAudioEngine ──> Checkpoints (silent/broken)
New (Single-Path):
Microphone
└─> AVAudioEngine ──> Audio buffers
├─> RMS calculation (silence detection)
├─> Checkpoint files (every 30s)
└─> Main recording (at end)
Why This Is Actually Simpler
Current complexity:
- AVAudioRecorder writes to temp file
- Read file back from disk
- Extract PCM data
- Apply normalization
- Return data
New simplicity:
- Accumulate audio buffers in memory
- Apply normalization
- Return data
Bonus: Checkpoints use the SAME audio as main recording. Guaranteed consistency.
What We’re Reusing
We already have all the infrastructure:
- ✅
createWAVDataFromFloats()- Converts buffers to WAV - ✅
normalizeAudioData()- Normalizes audio levels - ✅
processAudioBuffer()- Processes tap buffers - ✅
accumulatedAudioData- Buffer accumulation pattern
We just need to:
- Add main recording buffer alongside checkpoint buffer
- Append to both in the same tap callback
- Write main recording at end using existing functions
- Remove AVAudioRecorder code
Lessons for Solo Builders and TPMs
1. “Temporary” Solutions Aren’t
The Pattern:
- Ship quick solution to unblock feature
- Add TODO comment to fix later
- “Later” never comes
- Tech debt compounds
The Fix:
- Recognize when “temporary” is becoming permanent
- Schedule tech debt fixes before they block features
- Don’t ship features that depend on broken foundations
2. Critical Mass Moments Are Real
Signs you’ve hit critical mass:
- ✅ TODO comments from months ago
- ✅ Workarounds piling up
- ✅ New features breaking because of old decisions
- ✅ “We should really fix that” conversations
Action: Stop adding features. Fix the foundation.
3. Pre-Launch Is the Best Time to Fix
Why fix before v1.0:
- No users affected by the fix
- No support burden during transition
- Clean slate for launch
- Build trust from day one
Why NOT to wait:
- Users encounter broken features
- Support burden increases
- Harder to fix with production data
- Trust damage is permanent
4. Use the Decision Framework
Don’t rely on gut feel. Use structured analysis:
- Impact Severity - How bad is it?
- Workaround Feasibility - Can we ship around it?
- User Trust Impact - What’s the reputation cost?
- Fix Complexity - How hard to fix properly?
- Timeline Impact - Does it delay launch?
Score each factor. Set a threshold. Make the decision.
The Bottom Line
Shipping a broken feature is worse than delaying launch.
We could have shipped auto-recovery with silent checkpoint files. We could have added a disclaimer: “Recovery may not work perfectly.” We could have hoped users wouldn’t notice.
But that’s not how you build trust.
The right decision:
- Fix the underlying architecture
- Ship when it works properly
- Build on a solid foundation
The wrong decision:
- Ship broken features
- Hope users don’t notice
- Fix it “later” (with users affected)
What’s Next
We’re implementing the single-path architecture in the current branch. No feature flags, no half-measures. Clean implementation, comprehensive testing, ship when it’s ready.
Timeline: 3-5 days Risk: Manageable with proper testing Reward: Working auto-recovery + clean architecture for future features
Launch when ready, not when rushed.
Key Takeaways
For Solo Builders:
- Use the decision framework to evaluate tech debt severity
- Pre-launch is the best time to fix architectural issues
- Don’t ship features that depend on broken foundations
- “Temporary” solutions compound into permanent problems
For TPMs:
- Structured risk analysis beats gut feel
- User trust is more valuable than shipping fast
- Technical debt has a critical mass point
- Sometimes the right decision is to stop and fix the foundation
For Everyone:
- Recognize critical mass moments
- Fix before launch, not after
- Build trust through working features
- Clean architecture enables future velocity
This is part of our ongoing series on technical decision-making for solo builders. See also: Risk Assessment Framework and The Solo Builder’s Guide to Safe Tech Debt Cleanup.