top of page

Move Function or Module — Refactoring Patterns, Part 5

Shift Quality Editorial
Jul 29
2 min read

Updated: Oct 4

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