← Back to Desktop app developers

Skill · Version 1.0.0

Decompose modules

Splits tangled code along real responsibility boundaries while keeping public behavior and call sites stable.

module-decomposition

Team members using this skill

See how it helps

When is it useful?

Use when a large module mixes calculations, transport and side effects.

What does it help you do?

  • Finds cohesive domain logic and makes implicit state explicit.
  • Extracts narrow helpers before moving them across file boundaries.
  • Preserves import compatibility and checks for circular dependencies.

Try asking this

Separate invoice calculations from database and email orchestration while retaining existing service behavior and tests. Extract a pure total calculation first and compare invoice outputs. Move orchestration only after dependency boundaries are explicit and existing callers still work.

Detailed documentation

Read the skill’s original instructions, examples and checks in their authored language.

Details…Show less

Module Decomposition

When a module or function grows into a "God object" that mixes transport, validation, business rules, data access and formatting, decomposing it requires choosing boundaries that reduce system complexity rather than scattering the same tangle across ten tiny files.

1. Deep Modules vs. Shallow Fragmentation (John Ousterhout)

  • Aim for deep modules: a good module hides substantial internal logic behind a small, simple, stable interface. Callers should need to know what the module accomplishes, not how it sequences internal steps.
  • Avoid classitis and pass-through layers: do not split a 400-line function into eight 50-line files that all pass the same 12 parameters to one another or require callers to orchestrate five steps in sequence.
  • Measure coupling after the split: if modifying one rule still requires editing all newly split files in lockstep (shotgun surgery), the boundary was drawn along arbitrary lines instead of true cohesion.

2. Finding Natural Cohesion Axes

Separate responsibilities along boundaries that change for different reasons (Parnas's information hiding):

  1. Pure Domain Calculation vs. Side-Effectful Orchestration (Functional Core, Imperative Shell):
    • Extract pricing, permission evaluation, state transitions, diffing, scheduling rules and data transformations into pure functions or domain objects that take plain data in and return decisions/results out without touching I/O.
    • Keep the outer service or handler thin: load data via I/O, call the pure domain logic, and persist/emit the resulting effects.
  2. Transport / Boundary Adaptation vs. Application Logic:
    • Separate HTTP/CLI/IPC request parsing, auth context extraction and response serialization from the core use-case service.
  3. Sub-domain / Aggregate Boundaries:
    • When a single class manages unrelated concepts (e.g., UserService handling authentication, billing subscriptions, avatar uploads and notification preferences), split along cohesive domain capabilities that own distinct data subsets.

3. Breaking Circular Dependencies

When extracting modules reveals circular imports (A -> B -> A):

  • Extract shared pure types/utilities to a leaf module (A -> C and B -> C) when both modules genuinely share lower-level domain primitives.
  • Invert the dependency (Dependency Inversion): if high-level policy A calls low-level mechanism B which calls back into A, define a narrow interface or callback in A that B implements or receives.
  • Merge misplaced fragments: if A and B cannot function without reaching into each other's private state on every call, they are likely two halves of a single cohesive responsibility that should live together.

4. Step-by-Step Decomposition Workflow

  1. Prepare in place first: within the existing file, group related functions and state, eliminate shared mutable locals, and turn implicit state into explicit parameters and return values.
  2. Extract pure helpers locally: verify that extracted functions have narrow signatures and pass all characterization tests before moving them across files.
  3. Move cohesive units to the new module: keep re-exports or thin delegation in the original module temporarily if external callers depend on the old import path, then migrate callers in verifiable batches.
  4. Verify interface ergonomics: check that the new module's public exports are minimal, internal helpers remain unexported, and no circular dependencies were introduced.

Included files 1

  • SKILL.md