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...
- 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...
- 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...
Two common legacy code traps:
JAVACodeLoading syntax highlighter...
processedOrders list is static - shared across all instances and all tests! When tests run together, order IDs from one test "leak" into another.JAVACodeLoading syntax highlighter...
PaymentGateway - if this makes real API calls or has its own static state, tests become unpredictable.JAVA(38 lines)CodeLoading syntax highlighter...
š» Exercises
Exercise 1: Identify Seams
ā Difficulty: Easy | ā±ļø Time: 10 minutes
JAVA(21 lines)CodeLoading syntax highlighter...
| 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 |
JAVA(17 lines)CodeLoading syntax highlighter...
Exercise 2: Scratch Refactoring
āā Difficulty: Medium | ā±ļø Time: 20 minutes
JAVA(37 lines)CodeLoading syntax highlighter...
JAVA(29 lines)CodeLoading syntax highlighter...
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
JAVA(20 lines)CodeLoading syntax highlighter...
JAVA(76 lines)CodeLoading syntax highlighter...
Exercise 4: Document Legacy Code
āāā Difficulty: Medium-Hard | ā±ļø Time: 20 minutes
JAVA(38 lines)CodeLoading syntax highlighter...
JAVA(54 lines)CodeLoading syntax highlighter...
Exercise 5: Full Legacy Improvement Workflow
āāāā Difficulty: Hard | ā±ļø Time: 30 minutes
JAVA(17 lines)CodeLoading syntax highlighter...
- Write characterization tests for current behavior
- Identify seams
- Add fraud detection using Sprout Class technique
- Integrate without modifying original logic
JAVA(45 lines)CodeLoading syntax highlighter...
JAVA(40 lines)CodeLoading syntax highlighter...
JAVA(61 lines)CodeLoading syntax highlighter...
JAVA(38 lines)CodeLoading syntax highlighter...
Interview Questions
Q1: How do you approach a legacy codebase?
- 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?
- 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 |