Patina in Code: When Age Adds Character (vs When It's Just Old)
A 40-year-old Rolex is more valuable. A 40-year-old VCR is worthless. Code is the same. Some codebases age like wine (stable, trusted, respected). Others age like milk (brittle, scary, rewritten every 3 years). What's the difference? Simplicity, consistency, tests that help, just-enough docs, maintained dependencies, backward compatibility. Learn when to keep (patina), refactor (polish), or rewrite (rot).

TL;DR
Some codebases age beautifully (stable, trusted, simple) while others rot (brittle, scary, avoided). Patina comes from clear architecture, consistent patterns, good tests, and stable interfaces. Design for 10-year timelines by prioritizing simplicity, documentation, and graceful updates. Code can age like wine, not milk.
Patina in Code: When Age Adds Character (vs When It's Just Old)
There's a vintage Rolex on my wrist. It's 40 years old. It's more valuable now than when it was new.
In the next room: a 40-year-old VCR. Worthless. Maybe $10 at a garage sale.
Both are old. One aged beautifully. One just... aged.
Code is the same.
Some codebases age like wine—stable, reliable, trusted. The team treats them with respect.
Other codebases age like milk—brittle, scary, avoided. The team rewrites them every 3 years.
What's the difference?
Let's talk about patina—the character that comes with age—and how to design code that ages gracefully instead of rotting.
Two 10-Year-Old Codebases
Codebase A: Vintage (Good Old)
Story:
- Built 10 years ago
- Core API serving 10M requests/day
- Zero downtime in 3 years
- Team loves working on it
- New engineers ramp up in days
Characteristics:
- Simple architecture: 3 layers (API → service → data), easy to reason about
- Clear conventions: Every endpoint follows the same pattern
- Well-tested: 85% coverage, tests are fast and reliable
- Good documentation: RESTful API docs, architecture diagrams, runbooks
- Modern enough: Runs on current Node.js LTS, updated dependencies quarterly
- Stable interfaces: Versioned APIs, backward compatibility maintained
When someone says "we should rewrite this," the team says:
"Why? It works. It's stable. We understand it. Let's focus on new features."
This is patina. Age has added character, trust, and stability.
Codebase B: Legacy (Bad Old)
Story:
- Built 10 years ago
- Core system handling orders
- Crashes weekly
- Team dreads touching it
- New engineers take 3 months to understand it
Characteristics:
- Chaotic architecture: Layers mixed, business logic everywhere
- Inconsistent patterns: Every module written differently
- Barely tested: 15% coverage, tests are slow and flaky
- No documentation: "Just read the code" (but the code is incomprehensible)
- Outdated stack: Running Node.js 8 (unsupported for 5 years), dependencies with known security issues
- Brittle interfaces: Change one thing, 10 things break
When someone says "we should rewrite this," the team says:
"Please. But we don't have time. And we're scared of what we'll break."
This is rot. Age has added complexity, fear, and fragility.
What Makes Code Age Well?
Quality 1: Simplicity
Vintage code:
- Simple architecture (3-5 layers, clear boundaries)
- Simple data models (normalized, minimal joins)
- Simple patterns (one way to do things)
Why it ages well:
- Easy to understand 5 years later
- Easy to change without breaking things
- Easy to onboard new engineers
Rotten code:
- Complex architecture (10+ layers, unclear boundaries)
- Complex data models (denormalized chaos, circular dependencies)
- Complex patterns (every engineer invented their own style)
Why it rots:
- No one understands it
- Changes break unpredictable things
- New engineers need months to ramp up
Rule: Simple code ages gracefully. Complex code becomes legacy.
Quality 2: Consistency
Vintage code:
- Every API endpoint follows the same pattern
- Every error is handled the same way
- Every test is structured the same way
Example (REST API):
GET /api/v1/users
POST /api/v1/users
GET /api/v1/users/:id
PATCH /api/v1/users/:id
DELETE /api/v1/users/:id
Clean. Predictable. Boring.
Rotten code:
- Half the endpoints follow REST, half follow RPC, some are just chaos
- Errors are thrown 10 different ways
- Tests are a mix of unit, integration, and "I don't know what this tests"
Example (API chaos):
GET /getUsers
POST /createUser
GET /user/:id/fetch
PUT /update-user
POST /deleteUser ← why is DELETE a POST?
Inconsistent. Unpredictable. Scary.
Rule: Consistency makes code age well. Chaos creates legacy.
Quality 3: Tests That Actually Help
Vintage code:
- Tests that run in < 1 minute
- Tests that fail only when something is actually broken
- Tests that document expected behavior
Rotten code:
- Tests that take 30 minutes (no one runs them)
- Tests that fail randomly (ignored)
- Tests that don't explain what they're testing
The difference:
- Vintage tests give confidence to change things
- Rotten tests (or no tests) make everyone afraid to touch code
Rule: Good tests let code evolve. No tests (or bad tests) freeze code in place.
Quality 4: Documentation (But Not Too Much)
Vintage code has:
- Architecture diagram: Shows system structure
- API docs: RESTful endpoints, request/response examples
- Runbooks: How to deploy, how to debug common issues
- Decision records: Why we chose X over Y
Rotten code has either:
- No documentation: "Just read the code"
- Too much documentation: 500 pages, outdated, no one reads it
The sweet spot:
- Just enough documentation to onboard someone new
- Living documentation (maintained as code changes)
- Not trying to document every line of code
Rule: Document the "why" and the "how to get started." Don't document the "what" (that's what code is for).
Quality 5: Maintained Dependencies
Vintage code:
- Dependencies updated quarterly
- Running on current LTS versions of languages/frameworks
- Security patches applied promptly
Rotten code:
- Dependencies not updated in 5 years
- Running on unsupported versions (Node 8, Python 2.7, PHP 5)
- Known security vulnerabilities (ignored)
Why dependencies matter:
- Security: Old dependencies have exploits
- Compatibility: Hard to hire engineers for dead tech
- Performance: Modern runtimes are faster
Rule: You don't need bleeding-edge. But you need "not dead."
Quality 6: Backward Compatibility
Vintage code:
- Versioned APIs (v1, v2)
- Deprecation warnings (6 months notice before removal)
- Smooth migrations
Example:
GET /api/v1/users ← still works
GET /api/v2/users ← new, better, but v1 still supported for 1 year
Rotten code:
- Breaking changes with no warning
- No versioning (every change breaks clients)
- Migrations are "figure it out yourself"
Why it matters:
- Vintage code evolves without breaking things
- Rotten code forces painful rewrites
Rule: Maintain backward compatibility. Deprecate gracefully.
The Gray Area: When to Modernize vs When to Preserve
Not all old code needs to be rewritten.
Not all old code should stay untouched.
When to Keep It (Patina)
Keep old code if:
- It's stable (doesn't crash)
- It's understandable (new engineers can read it)
- It's tested (changes don't break things)
- It's maintained (dependencies are updated)
- It handles a stable domain (not changing)
Example:
- Authentication service built 8 years ago
- Zero downtime in 2 years
- Team understands it
- Dependencies are up to date
Action: Keep it. Focus on new features.
When to Refactor (Polish)
Refactor if:
- It's stable but hard to understand
- Tests are missing or bad
- Dependencies are outdated but migration is easy
Example:
- Payment processing service built 5 years ago
- Works, but code is messy
- No tests
- Running Node 14 (not terrible, but not current)
Action: Incremental refactor. Add tests. Update dependencies.
Not a rewrite. Just polish.
When to Rewrite (Rot)
Rewrite if:
- It's unstable (crashes frequently)
- It's incomprehensible (no one understands it)
- It's untestable (can't add tests without rewriting)
- It's blocking critical features (can't evolve)
- It's on dead tech (can't hire for it)
Example:
- Reporting service built 10 years ago
- Crashes weekly
- No one understands the code
- Built on PHP 5 (dead for years)
- Blocking new features
Action: Rewrite. But do it incrementally (strangler fig pattern).
How to Design Code That Ages Gracefully
Let's flip the script. How do you build code today that will age like wine, not milk?
Tactic 1: Design for Readability (Future You Won't Remember)
Write code assuming:
- You'll read this in 5 years
- You won't remember why you wrote it
- Someone else will need to change it
Clarity over cleverness:
Bad (clever but opaque):
const x = data.reduce((a,b)=>({...a,[b.k]:b.v}),{});
Good (clear):
const userMap = data.reduce((map, item) => {
map[item.key] = item.value;
return map;
}, {});
5 years from now, which one will you understand?
Tactic 2: Consistent Conventions (Pick One Way)
For every common pattern, decide once:
- Error handling: Throw exceptions? Return error objects?
- Async: Promises? Async/await?
- File structure: Where do tests live? Where do utilities live?
- Naming: camelCase? snake_case?
Write it down. Enforce it with linters.
Result: Code looks the same 5 years later.
Tactic 3: Versioned APIs (Plan for Change)
From day one:
- Version your APIs (
/api/v1/...) - When you make breaking changes, ship
v2(don't breakv1) - Deprecate old versions gracefully (6-12 months notice)
Result: Your system evolves without breaking clients.
Tactic 4: Write Tests That Document Behavior
Tests should answer:
- What does this code do?
- What are the edge cases?
- What breaks it?
Good test:
test('returns 404 when user does not exist', async () => {
const response = await request(app).get('/api/users/999');
expect(response.status).toBe(404);
expect(response.body.error).toBe('User not found');
});
5 years from now, you'll know exactly what this endpoint does.
Tactic 5: Quarterly Dependency Updates
Set a calendar reminder:
- Every 3 months: Update dependencies
- Check for security vulnerabilities
- Migrate to current LTS versions
Why quarterly?
- Not so often it's disruptive
- Not so rare that updates are huge and scary
Result: Your code stays modern without constant churn.
Tactic 6: Decision Records (Why Did We Do This?)
When you make a significant architectural decision:
Write a decision record:
- Context: What problem were we solving?
- Decision: What did we choose?
- Alternatives: What else did we consider?
- Consequences: Trade-offs
Example:
# ADR-001: Use Postgres for primary data store
Context: Need a relational database for transactions.
Decision: Use Postgres.
Alternatives: MySQL, MongoDB
Consequences: Strong consistency, ACID guarantees, but no horizontal sharding out of the box.
5 years from now, you'll remember why.
Closing: Age Is Not the Enemy
Old code isn't bad code.
Rotten code is bad code.
Some of the best code I've worked with is 10+ years old. It's:
- Simple
- Consistent
- Tested
- Documented
- Maintained
It has patina—the character that comes from stability, trust, and thoughtful evolution.
The goal isn't to rewrite every 3 years.
The goal is to build code that's still respected 10 years from now.
Exercise: Audit Your Codebase
Pick your oldest service/module.
Score it (1-5 for each):
| Quality | Score (1-5) |
|---|---|
| Simplicity (easy to understand?) | |
| Consistency (patterns are uniform?) | |
| Tests (good coverage, reliable?) | |
| Documentation (enough to onboard?) | |
| Dependencies (up to date?) | |
| Stability (crashes rarely?) |
Score:
- 25-30: Vintage. Has patina. Keep it.
- 15-24: Needs polish. Refactor incrementally.
- Below 15: Rotten. Consider rewrite.
Old code can be beautiful.
Build systems that age gracefully.
Simplicity, consistency, and care—that's how code earns patina.
