Quality & Collections

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).

Ruchit Suthar
Ruchit Suthar
November 18, 20259 min read
Patina in Code: When Age Adds Character (vs When It's Just Old)

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 break v1)
  • 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.

Topics

code-qualitylegacy-codeaging-systemsmaintainabilityrefactoringtechnical-longevity
Ruchit Suthar

About Ruchit Suthar

Technical Leader with 15+ years of experience scaling teams and systems