top of page

Move Function or Module — Refactoring Patterns, Part 5

Shawn West
Jul 29
2 min read

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)

  1. Identify destination

  2. Move function

  3. Update imports / callers

  4. Adjust references (this becomes order.x instead of this.x)

  5. Run tests

  6. 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.

bottom of page