3-Post Series

Cleaning Up Legacy Code:
Dragging the Implicit Into the Light

Three posts from untangling a real legacy codebase — a data shape nobody owned, a monolith with nowhere to attach a test, and pricing rules hiding in stored procedures.

3 posts ~10 min total Andrew Judd — developer, consultant

1. The One Idea Behind the Series

Legacy code rarely fails because someone wrote an obvious bug. It fails because the system depends on things nobody ever wrote down: a data shape every layer agrees on but no type declares, pieces fused so tightly they can't be checked on their own, and business rules tucked into the one place nobody reads.

Every fix in this series is the same move applied at a different scale: take what the system silently assumes and force it into a place where it has to be stated, named, and checked.

The same move, three times:

  • Data — an untyped nested dictionary becomes a class the compiler can check
  • Structure — a solid monolith is split into parts with boundaries a test can grab
  • Rulessp_recalc_v2 becomes a PricingPolicy that says what it is

2. The Three Posts

Each post stands on its own, but they're written to be read in order — each one sets up the ground the next one needs.

3. Why the Order Matters

The sequence isn't editorial. Each step is the precondition for the next, and skipping ahead is how legacy cleanups stall.

  1. 1

    Make the data honest

    Typed shapes move breakage from runtime back to compile time, so everything after this step works with inputs and outputs you can trust.

  2. 2

    Carve seams

    Boundaries and interfaces let you stand one piece up on its own. No seams, nothing for a test to hold.

  3. 3

    Add tests

    With seams in place, tests become possible — and they become the net for the risky work.

  4. 4

    Do the scary refactors

    Lifting live business logic out of the database stops being an act of faith once the first three steps are done.

What happens if you reverse it:

Reach for tests before there are seams and there's nothing to attach them to. Reach for the big refactor before there are tests and you're working without a net over exactly the code most likely to drop you.

4. Signs Your Codebase Needs This

If any of these sound familiar, one of the three posts is about your codebase:

Renames break things the build didn't catch

A field changes on the server, the build is green, and the failure shows up in the browser as a support ticket. That's a shape with no type owning it.

Read: The Invisible Contract →

"We'll add tests later" has been the plan for years

Every attempt to test one behaviour turns into wiring up the database and half the app. The problem isn't discipline — it's that there's nowhere to attach a test.

Read: No Seams →

Nobody can say where a business rule actually lives

Answering "what does this actually do?" means reading SQL in one system and application code in another, and holding both in your head at once.

Read: The Worst Room in the House for Business Logic →

5. Frequently Asked Questions

Where do you start when cleaning up legacy code?

Make the data honest first: give implicit shapes real types. Then carve seams, then add tests, and only then attempt the risky refactors. Reversing that order is how cleanups stall.

What is an implicit contract in code?

A data structure multiple parts of a system depend on but no type declares — like a nested dictionary serialized to JSON and read by key in the browser. Renaming a key compiles cleanly and breaks at runtime. Giving the shape a named class or record lets the compiler catch it.

Why can't I just add tests to my legacy code later?

A tightly coupled codebase has no seams — no boundaries where a test can isolate one piece. "Untested" is the symptom; coupling is the disease. Introducing boundaries and interfaces comes first, and those boundaries are what make tests possible.

Is putting business logic in stored procedures bad?

Rules in stored procedures are invisible from the application, hard to test with normal tooling, and often split between SQL and app code. SQL is excellent at set operations over data; business rules are conditionals and domain concepts that belong in application code as named, testable classes.

How do I move logic out of stored procedures safely?

Do it last. Type the inputs and outputs, carve seams and add tests around the behaviour, then lift the rule into a named class in the application — sp_recalc_v2 becomes a PricingPolicy — and verify it against the tests before retiring the procedure.

Stuck With a Codebase Like This?

Legacy systems can be untangled without a rewrite.

If you've got a system that runs the business but nobody wants to touch, reach out. We can usually work out where to start in a single conversation.