top of page

Split Phase — Refactoring Patterns, Part 8

Shift Quality Editorial
Jul 30
3 min read

Updated: Oct 4

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