Move Function or Module — Refactoring Patterns, Part 5
Updated: Jul 30
Refactoring Patterns · Part 5
A function often ends up in the wrong file — it lives with the code that first called it, not the code it actually belongs to, so every reader has to go hunting. Moving it to where it belongs is a small refactor with an outsized effect on how findable the codebase is. This walks through moving functions and modules safely, and avoiding the circular-import mess a careless move creates.
Functions belong with the data they touch. Move them when they don't.
Step 1: The Smell (15 min)
// in OrderService
class OrderService {
isOrderEligibleForDiscount(order: Order) {
return order.customer.tier === 'gold' && order.subtotal > 100
}
}
This function uses Customer + Order data but lives in OrderService. Why not on Order? Or Customer?
Step 2: After (15 min)
class Order {
isEligibleForDiscount() {
return this.customer.tier === 'gold' && this.subtotal > 100
}
}
Logic with the data it uses. Easier to find. Reusable.
Step 3: When to Move (15 min)
Function uses more data from class B than its own
Module is "everything else" / "utils" bucket
Logical home elsewhere
Step 4: Inter-Module Moves (15 min)
Files / modules organized by feature?
order-related code in orders/
payment-related code in payments/
A function spanning both? Move to whichever owns the action.
Step 5: The Steps (15 min)
Identify destination
Move function
Update imports / callers
Adjust references (this becomes order.x instead of this.x)
Run tests
Commit
IDEs help: "Move method" / "Move file."
Step 6: Refactor First (15 min)
If function is long / mixes concerns: break into smaller pieces first.
Then move pieces. Easier to find right home for each.
Step 7: Avoid Premature Moves (15 min)
Don't move just because:
"It's cleaner"
One file is bigger
Different layer
Move when:
Coupling reduces
Cohesion improves
Tests get easier
Step 8: Module Boundaries (15 min)
Modules:
Domain (orders, payments)
Layer (controllers, services, repos)
Capability (validation, notifications)
When moving across module boundary: deliberate decision. Sometimes signals a missing abstraction.
Step 9: Circular Imports Watch (15 min)
Moving A → B; now B → A → B circular?
Refactor:
Extract shared types to a third module
Reverse one direction (B doesn't really need A)
Dependency injection
Step 10: Pattern: Move Method to Aggregate (15 min)
In DDD:
Logic in services that operates on entities → move to entity
Entity grows; service shrinks
Anemic model becomes rich
What You Just Did
Move Function or Module: the smell, after, when to move, inter-module moves, the steps, refactor first, avoid premature moves, module boundaries, circular imports watch, pattern: move method to aggregate.
Common Failure Modes
Move without tests. Behavior change unnoticed.
Move and introduce circular import. Module breaks.
Move for aesthetics. No real win.
Forget to update callers. Broken build.
Anemic → still anemic. Just shuffled chairs.
Continue the Refactoring Patterns path
Part of the Refactoring Patterns learning path.


