Java

Working with Legacy Code

Working with legacy code is an essential skill. Most developers spend more time maintaining existing code than writing new code. This article covers techniques for understanding, documenting, and safely modifying code you didn't write.

📋 At a Glance

AspectDetails
DefinitionCode without tests, or code you're afraid to change
Key ChallengeUnderstanding before modifying
Primary ToolCharacterization tests
MindsetSurgical precision, not rewrites

🎯 What You'll Learn

  • Reading strategies for understanding unfamiliar code
  • Identifying seams for testing and modification
  • Sketch refactoring for exploration
  • Documentation techniques for knowledge capture
  • Safe modification strategies

Production Story: The Scary Codebase

A developer inherited a 50,000-line payment processing system:

JAVA(23 lines)
Code
Loading syntax highlighter...
First instinct: Rewrite everything Reality: Rewrite would take 6 months and introduce new bugs
The fix: Incremental improvement with characterization tests:
  1. Write tests that document current behavior
  2. Add seams for testability
  3. Make small, safe changes
  4. Repeat

Mental Model: Legacy Code Workflow

TEXT(41 lines)
Code
Loading syntax highlighter...

🔬 Deep Dive

Technique 1: Reading Strategies

JAVA(39 lines)
Code
Loading syntax highlighter...
Reading tips:
  • Read with a specific question in mind
  • Use IDE "Find Usages" liberally
  • Draw diagrams (sequence, class)
  • Take notes as you go

Technique 2: Scratch Refactoring

JAVA(35 lines)
Code
Loading syntax highlighter...

Technique 3: Identifying Code Structure

JAVA(41 lines)
Code
Loading syntax highlighter...

Technique 4: Finding Seams

JAVA(54 lines)
Code
Loading syntax highlighter...

Technique 5: Safe Modification Techniques

JAVA(60 lines)
Code
Loading syntax highlighter...

Technique 6: Documentation as You Go

JAVA(47 lines)
Code
Loading syntax highlighter...

Technique 7: Incremental Improvement

JAVA(37 lines)
Code
Loading syntax highlighter...

⚠️ Common Mistakes

Mistake 1: Trying to Understand Everything

TEXT(6 lines)
Code
Loading syntax highlighter...

Mistake 2: Big Bang Rewrites

TEXT(9 lines)
Code
Loading syntax highlighter...

🐛 Debug This: The Flaky Test

A developer added a characterization test for legacy code, but it fails intermittently. Sometimes it passes, sometimes it doesn't. "I don't understand - I'm testing the exact same code path!"

JAVA(49 lines)
Code
Loading syntax highlighter...
Tests pass individually but fail when run together. What's wrong?

✅ Solution:

Two common legacy code traps:

Problem 1: Static shared state
JAVA
Code
Loading syntax highlighter...
The processedOrders list is static - shared across all instances and all tests! When tests run together, order IDs from one test "leak" into another.
Problem 2: Hidden external dependency
JAVA
Code
Loading syntax highlighter...
Static method call to PaymentGateway - if this makes real API calls or has its own static state, tests become unpredictable.
Correct approach - identify seams and isolate:
JAVA(38 lines)
Code
Loading syntax highlighter...
The lesson: Legacy code often has hidden static state and static method dependencies. Always check for these when tests behave inconsistently!

💻 Exercises

Exercise 1: Identify Seams

⭐ Difficulty: Easy | ⏱️ Time: 10 minutes

Task: Identify all seams in this legacy class where you could inject test doubles.
JAVA(21 lines)
Code
Loading syntax highlighter...
List all seams and classify them (Object/Subclass/Preprocessing).
✅ Solution:
LocationTypeHow to Use
repository fieldObject SeamChange field access to constructor injection, inject mock
ConfigManager.getConfig()Preprocessing SeamExtract to protected getConfig() method, override in test
fetchData()Subclass SeamAlready protected! Override in test subclass
Formatter.format()No seam (static)Need to wrap in instance method first
EmailService.send()Preprocessing SeamExtract to protected sendEmail(), override
DatabaseConnection.getInstance()No seam (singleton)Already in fetchData() - use subclass seam
Testable refactor:
JAVA(17 lines)
Code
Loading syntax highlighter...

Exercise 2: Scratch Refactoring

⭐⭐ Difficulty: Medium | ⏱️ Time: 20 minutes

Task: Use scratch refactoring to understand this legacy method. Document what you discover, then discard your changes.
JAVA(37 lines)
Code
Loading syntax highlighter...
Document: What does this code do? What are the error codes?
✅ Solution:
Understanding after scratch refactoring:
JAVA(29 lines)
Code
Loading syntax highlighter...
Documentation:
DISCOVERED BEHAVIOR:
- Processes CREDIT and DEBIT transactions
- CREDIT: Adds amount to account (no balance check)
- DEBIT: Subtracts amount (requires sufficient balance)

ERROR CODES:
-1: Missing transaction type
-2: Insufficient funds (DEBIT only)
-3: Invalid account number (must be 10 chars)
-4: Invalid amount (must be > 0)
-5: Invalid transaction type (must be CREDIT/DEBIT)

RETURN VALUES:
"OK": Transaction successful
"NOOP": No operation performed
"ERROR:-X": Failed with error code X

NOTES:
- No input type is null - Map returns null for missing keys
- Account validation is only length check (no existence check!)
- Race condition: balance check and update not atomic

Exercise 3: Sprout Method Technique

⭐⭐ Difficulty: Medium | ⏱️ Time: 15 minutes

Task: You need to add audit logging to this legacy method. Use the Sprout Method technique to add the feature without modifying the existing logic.
JAVA(20 lines)
Code
Loading syntax highlighter...
Add audit logging for: start, validation result, gateway call, and final result.
✅ Solution:
JAVA(76 lines)
Code
Loading syntax highlighter...

Exercise 4: Document Legacy Code

⭐⭐⭐ Difficulty: Medium-Hard | ⏱️ Time: 20 minutes

Task: Write comprehensive documentation for this undocumented legacy class based on code analysis.
JAVA(38 lines)
Code
Loading syntax highlighter...
✅ Solution:
JAVA(54 lines)
Code
Loading syntax highlighter...

Exercise 5: Full Legacy Improvement Workflow

⭐⭐⭐⭐ Difficulty: Hard | ⏱️ Time: 30 minutes

Task: Apply the full legacy code improvement workflow to add a new feature (fraud detection) to this payment processor.
JAVA(17 lines)
Code
Loading syntax highlighter...
Steps:
  1. Write characterization tests for current behavior
  2. Identify seams
  3. Add fraud detection using Sprout Class technique
  4. Integrate without modifying original logic
✅ Solution:
Step 1: Characterization Tests
JAVA(45 lines)
Code
Loading syntax highlighter...
Step 2: Identify Seams
JAVA(40 lines)
Code
Loading syntax highlighter...
Step 3: Sprout Class for Fraud Detection
JAVA(61 lines)
Code
Loading syntax highlighter...
Step 4: Integrate Without Breaking Original
JAVA(38 lines)
Code
Loading syntax highlighter...

Interview Questions

Q1: How do you approach a legacy codebase?

Answer:
  1. Find entry points (controllers, main, scheduled tasks)
  2. Trace execution path for the feature I need to change
  3. Write characterization tests to document current behavior
  4. Identify seams for testability
  5. Make small changes with tests
  6. Document discoveries for next developer

Q2: What's a "seam" in legacy code?

Answer: A seam is a place where you can alter behavior without editing source code:
  • Object seam: Inject mock through constructor/setter
  • Subclass seam: Override protected method
  • Preprocessing seam: Extract factory method and override

📝 Summary

TechniquePurpose
Scratch RefactoringUnderstand through refactoring, then discard
Finding SeamsLocate points for test injection
Sprout MethodAdd new code in new method
Wrap MethodAdd behavior around existing method
Sprout ClassNew functionality in new class
Characterization TestsDocument current behavior

📅 Review Schedule for This Article

DayTaskTime
Day 1Review the Legacy Code Workflow diagram5 min
Day 3Redo Exercise 1 (Identify Seams)10 min
Day 7Practice scratch refactoring on code from your project15 min
Day 14Redo Debug This (The Flaky Test)10 min
Day 30Apply Sprout Method to add a feature in your codebase20 min

Next: [Part 20: Characterization Testing]