Introduction: What is Legacy Code and Why You Need to Refactor It
\n\nEvery Java developer sooner or later encounters legacy code — outdated, poorly structured, and poorly documented software. Such code is often written without unit tests, violating SOLID principles, and its maintenance becomes a nightmare. Refactoring legacy Java code is not just a whim, but a necessity for reducing technical debt and accelerating the development of new features.
\n\nIn this article, we will cover refactoring legacy Java code from A to Z: from identifying problematic areas to safely implementing changes using modern tools.
\n\nStage 1: Analysis and Assessment of Legacy Code
\n\nBefore changing code, you need to understand it. Here are the key analysis steps:
\n\n1.1. Collecting Metrics and Finding Code Smells
\n\nUse static analyzers:
\n- \n
- SonarQube — detects duplication, complex methods, code style violations \n
- PMD — finds potential bugs and dead code \n
- Checkstyle — checks compliance with formatting standards \n
Example of a typical code smell — a giant method:
\n\npublic class OrderProcessor {\n public void process(Order order) {\n // 200 lines of code: validation, discount calculation, sending email, logging...\n // Split this into small methods!\n }\n}\n\n1.2. Building a Dependency Map
\n\nLegacy code often contains circular dependencies and God classes. Use visualization tools:
\n- \n
- IntelliJ IDEA Dependency Viewer — built-in tool \n
- Structure101 — professional solution for architecture analysis \n
Stage 2: Creating a Safety Net of Tests
\n\nWithout tests, refactoring is playing Russian roulette. Your goal is to cover key scenarios with Characterization Tests (tests that capture current behavior).
\n\n2.1. The Golden Master Technique
\n\nFor large blocks of code without tests:
\n- \n
- Run the system with real data \n
- Save the output (logs, API responses) as a baseline \n
- After refactoring, compare the result with the baseline \n
2.2. Example Test for a Legacy Class
\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class LegacyCalculatorTest {\n \n @Test\n public void testLegacyCalculation() {\n LegacyCalculator calc = new LegacyCalculator();\n // Capture current behavior\n int result = calc.calculate(10, 5);\n assertEquals(15, result); // If there's a bug, the test preserves the bug!\n }\n}\n\nImportant: first commit the tests, then refactor. A green test is your insurance.
\n\nStage 3: Step-by-Step Refactoring with Patterns
\n\nRefactoring legacy Java code is not about "rewriting everything," but a series of small, safe transformations.
\n\n3.1. Extract Method
\n\nThe safest refactoring. The IDE does this automatically.
\n\nBefore:
\npublic void printInvoice(Invoice invoice) {\n double total = 0;\n for (Item item : invoice.getItems()) {\n total += item.getPrice() * item.getQuantity();\n }\n double discount = total > 100 ? total * 0.1 : 0;\n double finalTotal = total - discount;\n System.out.println("Total: " + finalTotal);\n}\n\nAfter (with extracted methods):
\npublic void printInvoice(Invoice invoice) {\n double total = calculateTotal(invoice);\n double discount = calculateDiscount(total);\n double finalTotal = total - discount;\n printTotal(finalTotal);\n}\n\nprivate double calculateTot