Map dependencies and coupling in legacy codebase to understand what breaks when you change something and identify refactoring risks
Before changing code, know what depends on it. Dependency mapping reveals the blast radius of your changes.
Core principle: Understand coupling before changing. High coupling = high risk. Map first, change second.
Map dependencies when:
What: Code explicitly imports/requires another module
import { UserService } from './services/user'; // Direct dependency
class OrderController {
constructor(private userService: UserService) {}
// OrderController depends on UserService
}
Tools:
# Find what imports this file (JavaScript/TypeScript)
grep -r "import.*UserService" --include="*.ts" --include="*.js"
# Find what imports this file (C#)
grep -r "using.*UserService" --include="*.cs"
# Find what this file imports
grep "^import" src/services/user.ts # JS/TS
grep "^using" src/Services/UserService.cs # C#
What: Components share data structures or database tables
UserService ā users table ā AuthService
Both depend on users table structure. Schema change affects both.
How to find:
# Find all SQL queries against table
grep -r "FROM users" --include="*.ts"
grep -r "users\." --include="*.sql"
What: Component A must run before Component B
// Migrations must run in order
001_create_users.sql
002_add_email_to_users.sql // Depends on 001 existing
Red flag: Race conditions if order not guaranteed
What: Code depends on external service being available
class PaymentService {
async charge() {
await this.stripe.charge(); // Runtime dependency on Stripe API
}
}
Impact: Service down = feature down
Tool: dependency-cruiser (JavaScript/TypeScript)
npm install -g dependency-cruiser
depcruise --include-only "^src" --output-type dot src | dot -T svg > deps.svg
Tool: madge (JavaScript)
npm install -g madge
madge --image deps.png src/
Tool: NDepend (.NET)
# Commercial tool for .NET dependency analysis
# Generates dependency graphs and metrics
# https://www.ndepend.com/
Tool: grep (any language)
# What depends on UserService? (JavaScript/TypeScript)
grep -r "UserService" --include="*.ts" --include="*.js" src/
# What depends on UserService? (C#)
grep -r "UserService" --include="*.cs" src/
# What does UserService depend on?
grep "^import" src/services/UserService.ts # JS/TS
grep "^using" src/Services/UserService.cs # C#
Technique: Add logging
class UserService {
findById(id: string) {
console.log(`UserService.findById called from ${new Error().stack}`);
// ...
}
}
Run application, see who calls what.
Find shared tables:
# Which files query users table?
grep -r "FROM users" --include="*.ts" --include="*.sql"
# Result shows coupling through data
Technique: Files that change together
# Files changed together with UserService
git log --format="" --name-only -- src/services/UserService.ts | \
sort | uniq -c | sort -rn | head -20
Files that change together often = coupled.
OrderController
ā UserService
ā Database (users table)
ā CacheService (Redis)
ā PaymentService
ā Stripe API
ā Database (payments table)
| UserService | PaymentService | EmailService | |
|---|---|---|---|
| OrderController | ā | ā | ā |
| UserController | ā | ā | |
| AdminController | ā | ā |
Row depends on columns.
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Controllers (HTTP Layer) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Services (Business Logic) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Repositories (Data Access) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Database ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Violations: Controller ā Repository directly (skips Service layer)
UserService is depended on by:
- OrderController
- AuthController
- AdminController
Ca = 3 (high coupling - many depend on it)
High Ca = Risky to change (many things break)
OrderController depends on:
- UserService
- PaymentService
- EmailService
- NotificationService
Ce = 4 (high coupling - depends on many)
High Ce = Hard to test (many dependencies to mock)
UserService: Ca=5, Ce=2
I = 2/(5+2) = 0.28 (stable - more depended on than depending)
UtilityFunction: Ca=0, Ce=10
I = 10/(0+10) = 1.0 (unstable - all dependencies, no dependents)
I near 0: Stable (hard to change safely) I near 1: Unstable (easy to change, low impact)
UserService depended on by 50 files
Problem: Change breaks everything Solution: Split into smaller services
A ā B ā C ā A (cycle!)
Problem: Can't change one without others Solution: Break cycle with interface/event
Change "user email format" requires editing:
- UserService.ts
- UserController.ts
- UserValidator.ts
- EmailService.ts
- NotificationService.ts
...20 more files
Problem: Single concept scattered across many files Solution: Extract into single place
Controller ā Database directly (skipping Service layer)
Problem: Violates architecture, hard to test Solution: Enforce layer boundaries
JavaScript/TypeScript:
# Count dependencies (imports) per file
find src -name "*.ts" -exec sh -c \
'echo "$(grep -c ^import "$1") $1"' _ {} \; | sort -rn
# Find files with >20 imports (high coupling)
find src -name "*.ts" -exec sh -c \
'count=$(grep -c ^import "$1"); [ $count -gt 20 ] && echo "$count $1"' _ {} \;
# Find most-imported files (high Ca)
grep -rh "^import.*from" --include="*.ts" src/ | \
sed "s/.*from ['\"]//;s/['\"].*//" | \
sort | uniq -c | sort -rn | head -20
C#/.NET:
# Count dependencies (usings) per file
find . -name "*.cs" -exec sh -c \
'echo "$(grep -c "^using " "$1") $1"' _ {} \; | sort -rn
# Find files with >20 usings (high coupling)
find . -name "*.cs" -exec sh -c \
'count=$(grep -c "^using " "$1"); [ $count -gt 20 ] && echo "$count $1"' _ {} \;
# Find most-used namespaces (high Ca)
grep -rh "^using " --include="*.cs" . | \
sed "s/using //;s/;.*//" | \
sort | uniq -c | sort -rn | head -20
Bad: "This looks simple" ā Change ā 10 things break Good: Map dependencies ā Understand impact ā Change safely
Bad: "No code imports it, safe to change" Good: "Check who queries this table first"
Data dependencies are invisible in import statements.
Step 1: Map dependencies
# Who imports UserService?
grep -r "UserService" --include="*.ts" src/
# Result: 35 files depend on it (Ca = 35)
# What does UserService import?
grep "^import" src/services/UserService.ts
# Result: Depends on DB, Cache, Logger (Ce = 3)
# Instability: I = 3/(35+3) = 0.08 (very stable = risky to change!)
Step 2: Analyze dependents
Controllers: 12 files
Services: 15 files
Background jobs: 5 files
Tests: 3 files
Step 3: Plan safe refactor
Option A: Big-bang (risky - 35 files break)
Option B: Add interface, change implementation (safer - 0 files break)
Option C: Strangler fig pattern (safest - gradual migration)
Decision: Option C (strangler fig) due to high coupling.