Table of Contents
Joel’s key observation: “It’s harder to read code than to write it.” Developers always think old code is messy. But that mess often encodes hundreds of real-world bug fixes, edge-case handling, and subtle behaviors that took years to discover and correct.
Core Lessons from “Things You Should Never Do”
The article’s central thesis can be summarized in one line:
Never throw away your working codebase and rewrite from scratch—unless you want to lose years of accumulated knowledge, introduce new bugs, and hand your competitors a gift.
But Joel’s advice goes deeper. Here are the crucial lessons, with practical implications for modern developers:
Old code is valuable. It’s been used, tested, and patched in production. It might be ugly, but those “ugly” lines are often hard-won bug fixes.
Architectural problems can usually be fixed incrementally. Refactoring, modularization, and targeted rewrites are less risky than a full reset.
Performance issues rarely require a full rewrite. Optimizing the slowest 1% of code yields 99% of the speedup.
Cosmetic or naming inconsistencies are trivial compared to lost functionality. Standardize conventions with scripts or IDE refactoring tools—not a rewrite.
You can’t outsmart history. A new team rewriting an old system will repeat old mistakes, miss hidden requirements, and learn the same painful lessons—at the customer’s expense.
Joel’s warning is echoed by modern studies and industry failures. As Forbes Tech Council notes, “poor software practices silently drain billions of dollars from organizations every year.”
Rewrites vs. Refactoring: Real-World Approaches
Let’s get concrete. Here’s how the two strategies compare in practice:
Approach
Pros
Cons
Typical Use Cases
Rewrite from Scratch
Opportunity to use modern languages and frameworks
Can fix deep architectural flaws—if you understand them
Loses all accumulated bug fixes and domain knowledge
High risk of missing subtle requirements
Long time to first shippable release
Morale and market risk
Small, well-understood codebases
Proof-of-concept or MVP
Incremental Refactoring
Preserves bug fixes and business logic
Enables continuous delivery and testing
Lower risk, faster feedback
Slower visible progress
Some technical debt persists
Production systems
Legacy code with business value
Systems with ongoing users and requirements
As Joel emphasized, most companies are better served by careful refactoring, not blank-slate rewrites.
Modern Case Studies & Code Examples
Let’s ground these lessons with practical code and real-world scenarios.
1. Legacy Bug Fix Handling
# Example: A “hairy” legacy function with mysterious bug fixes
def process_payment(order, payment_method):
# Initiate payment gateway (legacy code)
if payment_method == "CREDIT_CARD":
# workaround for gateway bug on weekends
if datetime.now().weekday() == 5 or datetime.now().weekday() == 6:
time.sleep(0.5) # legacy fix for rate limit
result = legacy_gateway.charge(order)
elif payment_method == "PAYPAL":
# legacy fix: retry on PayPal API timeout
for i in range(3):
try:
result = paypal_api.charge(order)
break
except TimeoutError:
if i == 2:
raise
time.sleep(1)
else:
raise ValueError("Unsupported payment method")
return result
# Note: In production, these “ugly” blocks are often there due to years of hard-won bug fixes.
If you rewrote this function from scratch, you’d likely omit these subtle fixes—leading to real-money failures in production.
Collaboration is key in refactoring legacy code safely.
2. Refactoring Instead of Rewriting
# Refactor: Extracting logic into smaller, testable functions
def process_payment(order, payment_method):
if payment_method == "CREDIT_CARD":
return _handle_credit_card(order)
elif payment_method == "PAYPAL":
return _handle_paypal(order)
else:
raise ValueError("Unsupported payment method")
def _handle_credit_card(order):
if datetime.now().weekday() >= 5:
_apply_weekend_fix()
return legacy_gateway.charge(order)
def _apply_weekend_fix():
time.sleep(0.5) # Maintain legacy fix
def _handle_paypal(order):
for i in range(3):
try:
return paypal_api.charge(order)
except TimeoutError:
if i == 2:
raise
time.sleep(1)
This approach makes the code easier to understand and test, while retaining the production-proven fixes.
3. “Cosmetic” Cleanups With Tools
# Example: Renaming variables across a large codebase with a script
import os
import re
def rename_member_vars(directory, old_prefix, new_prefix):
for root, dirs, files in os.walk(directory):
for file in files:
if file.endswith('.py'):
path = os.path.join(root, file)
with open(path, 'r') as f:
content = f.read()
new_content = re.sub(rf'\\b{old_prefix}(\\w+)', rf'{new_prefix}\\1', content)
with open(path, 'w') as f:
f.write(new_content)
# Usage: rename_member_vars('src/', '_', 'm_')
# Note: Always test on a non-production branch!
As Joel noted, these kinds of large-scale naming issues are better solved with tools and scripts—not by rewriting everything.
Comparison Table: Rewrite vs. Refactor Outcomes
Dimension
Rewrite
Refactor
Source/Notes
Time to Market
2-3 years (Netscape, Borland)
Continuous delivery possible
Joel on Software
Risk of Bugs
High (loses all bug fixes)
Lower (preserves fixes)
Joel on Software, Forbes Tech Council
Loss of Domain Knowledge
Almost certain
Minimal
Joel on Software
Morale Impact
Initial excitement, then burnout
Steady improvement
Industry case studies
Market Impact
Loss of leadership likely
Can retain/grow share
Joel on Software
Key Takeaways
Key Takeaways:
Never rewrite a production system from scratch without extraordinary justification.
Refactor incrementally to preserve bug fixes, business logic, and delivery cadence.
Use scripts and tools for cosmetic cleanups; don’t risk functionality for the sake of “pretty” code.
Architectural and performance problems can almost always be fixed in place.
Heed the lessons of industry giants—learn from Netscape, Borland, and Microsoft.
For a deeper dive, read Joel Spolsky’s original article and see recent analysis on the hidden costs of bad software practices. And for modern best practices in CI/CD, refactoring, and hardening, check our detailed guides on CI/CD Pipeline Wars 2026 and Helm Charts Security and Versioning .
Safe refactoring: preserve value, reduce risk, and deliver continuously.
Sources and References
This article was researched using a combination of primary and supplementary sources:
Primary Source
This is the main subject of the article. The post analyzes and explains concepts from this source.