top of page

Split Phase — Refactoring Patterns, Part 8

Shawn West
Jul 30
3 min read
Refactoring Patterns · Part 8

Some functions do two unrelated jobs tangled together — parse the input and compute the result, or fetch the data and format it — which makes both jobs harder to change and nearly impossible to test in isolation. Split Phase pulls them apart into stages connected by a clean intermediate. This walks through the refactor, and why the testing win alone usually pays for it.

One function doing two things is two functions trying. Split them.

Step 1: The Smell (15 min)

function processOrder(rawOrder: string) {
  // parse
  const [id, items, total] = rawOrder.split(',')
  // calculate
  const tax = parseFloat(total) * 0.08
  const shipping = items.length > 5 ? 0 : 10
  // persist
  db.save({ id, items, total, tax, shipping })
}

Three phases mashed together. Hard to test each.

Step 2: After (15 min)

function parseOrder(rawOrder: string): Order {
  const [id, items, total] = rawOrder.split(',')
  return new Order(id, items, parseFloat(total))
}

function applyTotals(order: Order): OrderWithTotals {
  const tax = order.total * 0.08
  const shipping = order.items.length > 5 ? 0 : 10
  return { ...order, tax, shipping }
}

function processOrder(rawOrder: string) {
  const order = parseOrder(rawOrder)
  const withTotals = applyTotals(order)
  db.save(withTotals)
}

Each phase: testable separately. Composable.

Step 3: The Two Phases (15 min)

Classic split:

  • Phase 1: parse / normalize / validate input

  • Phase 2: business logic on clean data

Each phase: simpler than mixed version.

Step 4: When to Apply (15 min)

  • Function does heterogeneous things

  • Hard to test pieces in isolation

  • Mixing I/O with logic

  • Mixing parsing with business rules

If you'd describe function with "and": split.

Step 5: Intermediate Data Structure (15 min)

The output of phase 1 becomes input to phase 2:

interface ParsedOrder {
  id: string
  items: Item[]
  total: number
}

Clean type. Phase 2 doesn't worry about input format.

Step 6: I/O at Edges (15 min)

Common pattern:

  • Phase 1 (top): I/O input

  • Phase 2 (middle): pure logic

  • Phase 3 (bottom): I/O output

Functional core, imperative shell.

Step 7: When NOT (15 min)

  • Trivial functions

  • One-off scripts

  • Performance-critical inner loops (allocation cost)

Most app code: benefits from split. Hot loops: profile first.

Step 8: Steps (15 min)

  1. Identify phases by adding comments / blank lines

  2. Extract phase 2 into function (Part 2: Extract Function)

  3. Define intermediate data structure

  4. Run tests

  5. Commit

  6. Extract phase 1 if needed

  7. Run tests

  8. Commit

Small steps.

Step 9: Pipeline Style (15 min)

const result = pipe(
  rawInput,
  parse,
  validate,
  applyDiscount,
  applyTax,
  save
)

Many split phases → pipeline. Each step's input is previous's output.

Step 10: Testing Wins (15 min)

test('parseOrder handles missing fields', () => { ... })
test('applyTotals applies 8% tax', () => { ... })
test('processOrder full flow', () => { ... })

Each phase has focused tests. Failure isolation easier.

What You Just Did

Split Phase: the smell, after, the two phases, when to apply, intermediate data structure, I/O at edges, when not, steps, pipeline style, testing wins.

Common Failure Modes

Over-split tiny functions. Noise.

Phase 2 still does I/O. Defeats purpose.

No intermediate type. Coupling persists.

Mix in retry / error handling badly. Cross-cutting smear.

Stop after first split. Missed deeper structure.

Continue the Refactoring Patterns path

Part of the Refactoring Patterns learning path.

bottom of page