When NOT to Rewrite: Why We Kept 1,722 Lines of TypeScript Inside a Rust App
The counterintuitive companion to the rewrite decision. The specific engineering principle that protects you from the second-system effect — and the test that decides what to port vs. what to leave alone.
Published: May 2026 Tags: #SoloBuilder #Rust #Rewrite #SecondSystemEffect #PortVerification #Engineering
The “So What?” - Why You Should Care
Bottom Line Value: A concrete principle (“Chesterton’s Fence for code”) and a specific 5-question test that decides which parts of your old codebase to port vs. preserve verbatim. The decision that saved us 6 weeks and one near-disaster.
What You’ll Gain:
- The 5-question test that decides “port to new stack” vs. “keep as-is”
- The cross-language module pattern (TS code invoked from a Rust app via Tauri)
- The “tuning surface” concept — code with years of latent expertise embedded in it
- A safer alternative to “rewrite everything because you’re rewriting anyway”
- ROI: 1,722 lines of code preserved that would have taken 6+ weeks to re-tune correctly — and would have shipped with subtle regressions you wouldn’t have noticed for months
Who This Helps: Solo builders mid-rewrite who are about to throw away code that “looks dated” but actually contains tacit knowledge they can’t easily reproduce.
Why Bother: The second-system effect kills rewrites. The most-cited reason: builders re-implement code that wasn’t broken, get the easy parts right, and miss the edge cases the original code learned over months of production use. Knowing what NOT to rewrite is the harder, more valuable discipline.
TLDR: The 5-Question Test
For every module in your old codebase, before deciding to port it, ask:
- Is it heavily tuned? Has it accumulated edge-case handling over months?
- Is the tuning encoded in the code rather than in your head? Could you re-derive the rules from scratch?
- Does it have a clean interface? Can you call it from anywhere with just data in/data out?
- Is the stack incidental to the logic? Would the algorithm work in any language?
- Are there tests that capture the behavior? Or can you generate snapshots?
Decision rule: 4+ yeses → keep it as-is. Port the interface, not the implementation.
We kept 1,722 lines of TypeScript inside our Rust app this way. Zero regressions. Six weeks saved.
The Moment You’re Probably In
You’ve made the rewrite decision (see post #135). You’re standing at a fresh repo with a clean slate. You feel powerful. You want to make every line of code beautiful and idiomatic in the new stack.
Then you open a module from the old codebase. Maybe it’s:
- A formatting algorithm with 14 numbered scoring rules in inline comments
- A retry-and-backoff routine that knows about specific failure modes from your API integrations
- A heuristics file that’s been adjusted 40 times based on user feedback
- A parser with 23 if-branches for edge cases you discovered over a year
Your instinct: “I’ll just port this. It’ll be cleaner in [new language].”
Stop. This is the moment where rewrites turn into disasters. Here’s the test.
The Engineering Principle: Chesterton’s Fence for Code
There’s a famous principle in design called Chesterton’s Fence: don’t tear down a fence until you know why it was put up.
For code, the corollary is: don’t rewrite a tuned module until you’ve enumerated every reason the existing tuning is what it is.
Most tuned modules in real software have:
- Comments explaining 30% of the decisions
- Tests covering 60% of the cases
- And the remaining ~40% of behavior is tacit knowledge embedded in the structure
You will not re-derive that 40% from scratch in any reasonable time. You will ship subtle regressions. Your users will report “it doesn’t feel right” without being able to articulate why. You will spend months chasing the missing tacit knowledge that you destroyed when you “modernized” the code.
The solution is asymmetric: preserve the tuned modules verbatim; rewrite only the integration surfaces around them.
The 5-Question Test (in detail)
For every meaningful module in your old codebase, ask:
Q1: Is it heavily tuned?
Look for:
- Many specific numeric constants (thresholds, weights, multipliers)
- Long lists of edge-case handlers
- Comments referencing user reports or bug numbers
- “DO NOT CHANGE without verifying X” warnings
Our example: NaturalFormatter.ts had paragraph-break scoring weights derived from analyzing 200+ user recordings over 12 months. Lines like:
const SCORE_AFTER_DISCOURSE_MARKER = 2.5; // tuned via /transcripts/2024-12 corpus
const SCORE_LONG_PAUSE_GAP = 1.8;
const MIN_BREAK_DISTANCE_MS = 1200;
Each number is an artifact of testing. Heavy tuning = strong “keep” signal.
Q2: Is the tuning encoded in the code rather than in your head?
Can you take a clean sheet and re-derive the rules without re-running the entire empirical history that produced them?
If the constants in the code surprise even you when you re-read it, the answer is no — you couldn’t re-derive them.
For our formatters: I (the author) couldn’t tell you the right value of MIN_BREAK_DISTANCE_MS without re-listening to 50 recordings. The code knew; I didn’t. Strong “keep” signal.
Q3: Does it have a clean interface?
Can the module be called from anywhere with just data in / data out? Or is it tangled with the rest of the codebase?
Tangled modules are bad keep candidates — they’d require you to also keep the tangled context, which becomes a parallel codebase.
Our formatters had a single clean function signature:
function format(transcription: Transcription, options: FormatterOptions): FormattedResult
No I/O, no global state, no environment dependencies. Perfect “keep” candidate.
Q4: Is the stack incidental to the logic?
If the same algorithm works equally well in Rust or TypeScript or Python, the language choice is incidental. You shouldn’t port for the sake of language consistency.
If the module relies on language-specific features (TS generics, Python decorators, Rust traits) in load-bearing ways, the language is structural — you’d lose more than you’d gain by porting.
Our formatters were essentially algorithmic — arrays, math, string manipulation. They’d work in any language. Language is incidental.
Q5: Are there tests that capture the behavior?
This is the safety net. Even if you decide to keep the module verbatim, you need to be able to verify nothing changes between v1 and v2.
If tests don’t exist, you can manufacture them via characterization tests — run the old module on a corpus of real inputs, capture the outputs as snapshots, then assert the new system produces identical outputs. See post #140 on golden-file characterization tests for the specific pattern.
For our formatters: we added 32 characterization snapshot tests across 13 edge-case scenarios. Test-able = safe to keep.
How We Actually Kept TypeScript Inside a Rust App
The architectural answer: the new stack envelops the old code, doesn’t replace it.
For us, Tauri 2 made this clean. Tauri runs a Rust backend and a webview frontend. The webview is JavaScript/TypeScript anyway. So:
- Rust handles: audio capture, transcription, state machine, platform integration, settings persistence, session storage
- TypeScript handles: UI, plus the 1,722-line formatting algorithms (run in the frontend’s JS runtime)
The data flow:
Rust transcribes audio → emits result via IPC →
TypeScript receives in webview → formatters process →
result rendered in UI
The formatters live in src/processing/:
src/processing/
├── NaturalFormatter.ts (1,123 lines)
├── ContextualParagraphFormatter.ts (599 lines)
└── __tests__/
├── formatter-characterization.test.ts (32 snapshots)
└── __snapshots__/
└── formatter-characterization.test.ts.snap
We ran npm test against the existing TS code in the old repo, copied the source files into the new repo unchanged, ran npm test again, asserted identical snapshots, and shipped.
Total time to “port”: ~30 minutes (mostly cp -r and updating import paths).
Total time saved vs. porting to Rust: 6+ weeks.
What This Doesn’t Fix
- Cross-language boundaries cost something. You’re now maintaining two languages. Plan tooling (test runners, linters, CI) for both.
- Refactoring across the boundary is harder. If you need to change the data structure that crosses Rust↔TS, you change two type definitions and a serde contract.
- Some users will say “why isn’t this all Rust?” Ignore them. Polyglot codebases are normal. Pure-language codebases are an aesthetic preference, not an engineering one.
- You can’t easily share types between the Rust and TS sides. Use codegen (e.g.,
ts-rsfor Rust→TS or schema-first via JSON Schema) if you need to. - The “kept” code still ages. It still needs maintenance. If you find a bug in the TS formatters 2 years later, you fix it in the TS. The decision to keep isn’t a decision to freeze.
The Anti-Patterns We Avoided
The traps the test protects you from:
Anti-pattern 1: “It’ll be cleaner in Rust.” Sometimes true. Often a rationalization for re-implementing tuned code from scratch. Cleaner != better when the original behavior is what users depend on.
Anti-pattern 2: “I’ll port it for consistency.” Polyglot codebases are normal in production. Tauri, Electron, Next.js, Django + JS frontend — all polyglot. Consistency is overrated as a goal.
Anti-pattern 3: “I’ll just rewrite the easy parts.” Easy parts are easy because someone made them easy. You’ll re-implement the easy parts correctly and miss the hard parts they protected.
Anti-pattern 4: “I’ll port and add tests as I go.” Tests written after the port don’t catch what the port broke. They capture the new behavior, including the regressions.
Anti-pattern 5: “I’ll port one module, see how it goes.” One module is a probe; the test is whether the probe finishes within budget. Most teams find the probe takes 3× the estimate, and then they’re committed to porting all the related modules to maintain consistency.
Try It This Week
Saturday afternoon (90 minutes):
- List every meaningful module in your old codebase
- For each, run the 5-question test
- Score yes/no on each question
Saturday evening (30 minutes):
- Mark every module 4+ yes as “keep verbatim”
- Mark every module 3 yes as “keep, but verify with characterization tests”
- Mark every module ≤2 yes as “safe to port”
Sunday morning (90 minutes):
- For the “keep verbatim” modules:
- Make sure they have tests (or can be characterized — see post #140)
- Plan the integration surface: how will the new code call into them?
Sunday afternoon:
- Implement integration for ONE kept module
- Run end-to-end. Assert identical output to the old system.
- This is your proof of concept that the polyglot approach works.
Week 2:
- Repeat for the rest of the kept modules
- Port the modules that scored low on the 5-question test
That’s the discipline. It feels weird to not rewrite during a rewrite. It’s also what separates rewrites that ship from rewrites that don’t.
The Takeaway
The second-system effect isn’t about over-engineering. It’s about under-respecting the tacit knowledge in code that “looks dated.”
Heavily-tuned code in your old codebase is the most valuable thing you have. It’s the encoded version of all the user feedback, edge cases, and “we tried that and it didn’t work” lessons your team learned. Throwing it away to be idiomatic in the new stack is the most expensive aesthetic decision you can make.
The fix is concrete:
- 5-question test to identify what’s tuned vs. what’s incidental
- Polyglot architecture (the new stack envelops the old code)
- Characterization tests to verify zero behavioral drift
- Discipline to leave working code alone
The principle: in a rewrite, the most valuable thing you can do is decide what NOT to rewrite. Most teams skip this step because it feels lazy. It isn’t. It’s the discipline that protects you from the second-system effect.
If you’re mid-rewrite and you’re about to throw away a module because it “feels old”: run the 5 questions first. The answer might be that the module isn’t old. It’s tuned. And tuning is the most valuable thing in your codebase.
Coming up next: post #140 — Golden-file characterization tests. Once you’ve decided what to keep, the next question is how to prove it still works the same way. The specific pattern for capturing existing behavior as test snapshots — even when you don’t have a written spec.