Legacy Code Modernization Roadmap
The Old Test Pyramid tells you that Unit Tests are most important. But I'll reveal to you a "secret": in Legacy Code, you should NOT start with Unit Tests.
Welcome to the premium Optivem Journal. I’ll help you apply TDD to escape the nightmare of Legacy Code. Join our paid community of 160+ senior engineers & leaders for support on your TDD journey, plus instant access to group chat and live Q&As:
📢 On Wed 9th July 17:00 CET, I’m hosting TDD & Unit Tests - Part 4 (Live Q&A):
P.S. If you’re a Paid Member, you’ll receive a 100% discount (see Event Description for instructions on how to redeem your 100% discount).
You don’t need to start with Unit Tests & Hexagonal/Clean Architecture.
Actually, it would be a big mistake to start with Unit Tests - the ROI will be too low!
To maximize ROI, start with Acceptance Tests first. They’ll help lead to visible improvement metrics - reduced regression bugs & reduced delivery time. The other great part is that it won’t force developers to change how they’re working - so you’ll get zero/low resistance from your team!
Do NOT start with Unit Tests.
Do NOT start with Hexagonal/Clean Architecture.
…. instead…
Start with Acceptance Tests.
Don’t touch the Big Ball of Mud.
It’s a message that I often repeat.
One of our readers, Marc Uriel BOKO, summarized it well:
During my learning journey in backend software testing, I explored various approaches to structuring tests, particularly comparing the Old and Modern Test Pyramids
The Old Test Pyramid emphasizes writing unit tests , followed by integration tests, and then end-to-end (E2E) tests. While this approach has value, I realized some of its limitations—especially how unit tests often become tightly coupled to implementation details. This leads to over-mocking, which makes tests fragile and harder to maintain when refactoring code, and sometimes the project is not unit testable.
Thanks to the insightful articles by Valentina (Cupać) Jemuović, I discovered the Modern Test Pyramid, which flips that perspective. It prioritizes high-level acceptance tests that verify the system from the outside-in, based on business and user expectations. These tests offer higher ROI, since they protect against user-facing regressions ,the core reason why we test in the first place.
One key takeaway that shifted my mindset: Acceptance tests do not require Hexagonal or Clean Architecture to be implemented. They can even be used effectively in legacy or monolithic applications. Architecture decisions like Hexagonal or Clean come into play later, especially when introducing unit tests around application-level business logic.
Here’s a simplified priority model I learned from Valentina’s work:
Priority 1 (essential for all projects): Acceptance Tests, Component Tests, Pipelines, and Contract Tests.
Priority 2 (context-dependent): Hexagonal Architecture and Unit Tests for business logic.
Priority 3 (advanced, context-dependent):Clean Architecture and DDD, when applicable.
This modern perspective helped me see software testing not as a rigid checklist, but as a strategy tailored to project needs and goals.
- Marc Uriel BOKO, LinkedIn post [quoted with minor formatting updates above]
Transformation Roadmap
Phase 1 [ESSENTIAL]: Pipeline, Acceptance Tests & External System Contract Tests
Setup Pipeline with:
Components - Commit Stages: automated build & publish artifact
System - Acceptance Stage: automated release to Acceptance Environment and executing of Acceptance Tests, External System Contract Tests, E2E Tests
System - Release Stage: automated release to UAT & Production Environment
Write a handful of Acceptance Tests to cover critical business scenarios (this will cause you to setup the Acceptance Test architecture)
Start applying ATDD for new User Stories & Bug Fixes (for each such backlog item, you’ll also need to retroactively write Acceptance Tests for affected functionality)
Phase 2 [ESSENTIAL]: Component Tests & Contract Tests
Update Pipeline:
Components - Commit Stages: after compiling code, add a placeholder for running Component Tests & Contract Tests, prior to building and publishing artifact
Choose a handful of highest priority Acceptance Tests. For those Acceptance Tests, retroactively write Component Tests & Contract Tests (this will cause you to setup the Component Test architecture)
Start applying TDD at the Component Level for new User Stories & Bug Fixes (this is a loop within your ATDD loop; furthermore as part of doing this, you’ll be forced to retroactively write missing Component Level Tests for the affected code behavior)
Phase 3 [CONTEXT DEPENDENT]: Hexagonal Architecture, Unit Tests & Narrow Integration Tests
Choose a Component that has the highest degree of business complexity
Update Pipeline:
Component - Commit Stage: after compiling the code, add a placeholder for running Unit Tests & Narrow Integration Tests, prior to running Component Tests
Introduce Hexagonal Architecture into that Component, to enable Unit Testability (it will cause you to separate Big Ball of Mud into Driving Adapters, Hexagon, Driven Adapters)
Choose a handful of highest priority Component Tests. For those Component Tests, retroactively write Unit Tests & Narrow Integration Tests (this will cause you to setup the Unit Test Architecture)
Start applying TDD at the Unit Level for new User Stories & Bug Fixes (this is a loop within your Component Level TDD loop; furthermore as part of doing this, you’ll be forced to retroactively write missing Unit Level Tests for affected code behavior)
After success with one Component, you may try this with other Components that have high business logic complexity
Phase 4 [CONTEXT DEPENDENT]: Clean Architecture & DDD
Choose a Component where you’ve applied Hexagonal Architecture, Unit Tests & Narrow Integration Tests, and you’re practicing TDD there. (If multiple Components have this, then choose the one with highest business logic complexity)
Restructure the Component from Hexagonal Architecture to Clean Architecture
Start applying DDD, e.g. moving from anemic entities to rich entities
After success with one Component, you may try this with other Components that have high business logic complexity
Want to get started?
I discovered this roadmap after making many mistakes. I started initially by just following Uncle Bob (trying to apply Unit Tests everywhere), then I realized it was a mistake. If you’re busy, don’t want to waste the next years in trial-and-error, I’ll show you step-by-step how to introduce TDD in Legacy Code in a practical way.
Have you found that starting with acceptance tests makes it easier to get buy-in for unit tests later?