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
| Aspect | Details |
|---|---|
| Definition | Code without tests, or code you're afraid to change |
| Key Challenge | Understanding before modifying |
| Primary Tool | Characterization tests |
| Mindset | Surgical 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)CodeLoading syntax highlighter...
First instinct: Rewrite everything
Reality: Rewrite would take 6 months and introduce new bugs
The fix: Incremental improvement with characterization tests:
- Write tests that document current behavior
- Add seams for testability
- Make small, safe changes
- Repeat
Mental Model: Legacy Code Workflow
TEXT(41 lines)CodeLoading syntax highlighter...
🔬 Deep Dive
Technique 1: Reading Strategies
JAVA(39 lines)CodeLoading 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)CodeLoading syntax highlighter...
Technique 3: Identifying Code Structure
JAVA(41 lines)CodeLoading syntax highlighter...
Technique 4: Finding Seams
JAVA(54 lines)CodeLoading syntax highlighter...
Technique 5: Safe Modification Techniques
JAVA(60 lines)CodeLoading syntax highlighter...
Technique 6: Documentation as You Go
JAVA(47 lines)CodeLoading syntax highlighter...
Technique 7: Incremental Improvement
JAVA(37 lines)CodeLoading syntax highlighter...
⚠️ Common Mistakes
Mistake 1: Trying to Understand Everything
TEXT(6 lines)CodeLoading syntax highlighter...
Mistake 2: Big Bang Rewrites
TEXT(9 lines)CodeLoading 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)CodeLoading syntax highlighter...
Tests pass individually but fail when run together. What's wrong?
✅ Solution:
Two common legacy code traps:
Problem 1: Static shared state
JAVACodeLoading 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
JAVACodeLoading 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)CodeLoading 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)CodeLoading syntax highlighter...
List all seams and classify them (Object/Subclass/Preprocessing).
✅ Solution:
| Location | Type | How to Use |
|---|---|---|
repository field | Object Seam | Change field access to constructor injection, inject mock |
ConfigManager.getConfig() | Preprocessing Seam | Extract to protected getConfig() method, override in test |
fetchData() | Subclass Seam | Already protected! Override in test subclass |
Formatter.format() | No seam (static) | Need to wrap in instance method first |
EmailService.send() | Preprocessing Seam | Extract to protected sendEmail(), override |
DatabaseConnection.getInstance() | No seam (singleton) | Already in fetchData() - use subclass seam |
Testable refactor:
JAVA(17 lines)CodeLoading 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)CodeLoading syntax highlighter...
Document: What does this code do? What are the error codes?
✅ Solution:
Understanding after scratch refactoring:
JAVA(29 lines)CodeLoading 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)CodeLoading syntax highlighter...
Add audit logging for: start, validation result, gateway call, and final result.
✅ Solution:
JAVA(76 lines)CodeLoading 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)CodeLoading syntax highlighter...
✅ Solution:
JAVA(54 lines)CodeLoading 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)CodeLoading syntax highlighter...
Steps:
- Write characterization tests for current behavior
- Identify seams
- Add fraud detection using Sprout Class technique
- Integrate without modifying original logic
✅ Solution:
Step 1: Characterization Tests
JAVA(45 lines)CodeLoading syntax highlighter...
Step 2: Identify Seams
JAVA(40 lines)CodeLoading syntax highlighter...
Step 3: Sprout Class for Fraud Detection
JAVA(61 lines)CodeLoading syntax highlighter...
Step 4: Integrate Without Breaking Original
JAVA(38 lines)CodeLoading syntax highlighter...
Interview Questions
Q1: How do you approach a legacy codebase?
Answer:
- Find entry points (controllers, main, scheduled tasks)
- Trace execution path for the feature I need to change
- Write characterization tests to document current behavior
- Identify seams for testability
- Make small changes with tests
- 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
| Technique | Purpose |
|---|---|
| Scratch Refactoring | Understand through refactoring, then discard |
| Finding Seams | Locate points for test injection |
| Sprout Method | Add new code in new method |
| Wrap Method | Add behavior around existing method |
| Sprout Class | New functionality in new class |
| Characterization Tests | Document current behavior |
📅 Review Schedule for This Article
| Day | Task | Time |
|---|---|---|
| Day 1 | Review the Legacy Code Workflow diagram | 5 min |
| Day 3 | Redo Exercise 1 (Identify Seams) | 10 min |
| Day 7 | Practice scratch refactoring on code from your project | 15 min |
| Day 14 | Redo Debug This (The Flaky Test) | 10 min |
| Day 30 | Apply Sprout Method to add a feature in your codebase | 20 min |
Next: [Part 20: Characterization Testing]