Refactor Safely
Refactoring is behaviour-preserving by definition. The moment behaviour changes, this stops being a
refactor and becomes a change that needs a design and a review.
Two rules make the difference between a refactor and an outage: pin the behaviour before you
touch it, and keep every step small enough to throw away. A large refactor that "works" but
cannot be reverted is a rewrite wearing a safer name.
Required Skills
Read and apply:
test-strategy — characterisation tests, and the right level to pin behaviour at. (always)
clean-architecture — the target structure and why it is better. (always)
collaborative-judgment — the scope, the target, and anything that turns out to be a behaviour
change. (always)
service-boundaries — module or service extraction. (conditional: moving code across a boundary)
domain-modelling — restructuring domain types and aggregate boundaries. (conditional)
data-access — schema changes, migrations, query restructuring. (conditional)
infrastructure-adapters — introducing or reshaping a port to create a seam. (conditional)
api-contracts — anything a consumer can observe. (conditional: a public surface is involved)
resilience-patterns — moving code across a process boundary turns a call into a network hop.
(conditional: extraction)
Workflow
Step 1 — Name the target and the reason
Before any change, state three things and get agreement:
- What is wrong now — the concrete pain, not an aesthetic judgment. "Adding a payment method
requires edits in six files" is a reason. "This does not follow SOLID" is not.
- What it should look like — the target structure, in terms of
clean-architecture.
- What will be measurably better — fewer files touched per change, a testable unit, a boundary
the compiler enforces.
If you cannot fill all three, STOP. Refactoring without a named force is churn: it produces a
large diff, a review burden, and a merge conflict for everyone else, in exchange for nothing.
Agree the scope boundary explicitly — which files may change, and which are out of bounds. A
refactor that spreads is one nobody can review.
Step 2 — Pin the current behaviour
You cannot preserve behaviour you have not captured.
- Run the existing tests. They must be green before you start. Failing tests → fix or
quarantine them first; you cannot tell your breakage from theirs otherwise.
- Assess the coverage of the code you are about to move. Read the tests; do not trust a
percentage (
test-strategy).
- Write characterisation tests for what is not covered. These pin what the code does, not
what it should do:
- Drive from the outside — the level where behaviour is observable without knowing the
structure you are about to change. Usually a component test.
- Include the ugly cases: nulls, empty collections, boundary values, error paths.
- If a characterisation test reveals a bug, do not fix it now. Pin the current behaviour,
note the bug, and raise it separately. Fixing behaviour during a refactor makes the diff
impossible to reason about.
- STOP if the code cannot be characterised at any level. That is itself the finding — say so,
and propose either adding a seam first or leaving it alone.
Step 3 — Transform in small steps
Each step is one transformation, verified, and independently revertible.
For every step:
- Make one change — rename, extract, inline, move, introduce a parameter object, replace a
conditional with polymorphism.
- Run the tests. Green → continue. Red → revert the step; do not debug forward. A revert costs
seconds; debugging a half-applied refactor costs an afternoon.
- Commit at each green point, so any step can be abandoned without losing the rest.
Rules that hold throughout:
- Never mix behaviour change with structure change. If a fix or improvement is needed, finish
the refactor, then make it as a separate change. If it cannot wait, stop the refactor, make the
behaviour change with its own tests, then resume.
- Keep the build green between steps. Long-lived broken states are how refactors get abandoned
half-done, leaving the codebase worse than before.
- Prefer parallel-change (expand → migrate → contract) for anything with callers: add the new
shape, move callers over one at a time, remove the old one. Each stage ships independently.
- Use the tooling. IDE and language-server refactorings are safer than hand editing for renames
and moves — but re-read the diff; automated moves miss reflection, strings, configuration, and
documentation.
- Update tests only for structure. A test whose assertions must change means behaviour
changed — stop and reconsider.
Apply the clean-architecture checklist after each significant step, and surface any
Ambiguity Signals through collaborative-judgment rather than deciding the target unilaterally.
Step 4 — Extraction (module or service)
Moving code across a process boundary is not a refactor of one codebase; it is a change to the
system's failure model. Follow the strangler-fig sequence in service-boundaries, and stop at each
stage:
- Module first. Move the code inside the existing deployable, behind an explicit interface.
Add an architecture test that fails the build on a boundary violation. Ship this. Most of
the risk lives here, and it is still cheap to undo.
- Separate the data. Route every access through the interface; nothing else touches those
tables. Ship this. If this proves impossible, the boundary was wrong — that is the cheapest
possible moment to learn it.
- Add the remote implementation of the same interface, called by nothing yet.
- Shift traffic behind a flag (
config-and-dependencies), comparing results, with an instant
rollback.
- Remove the local implementation only after the remote one has carried full traffic long
enough to trust.
At step 3, every call that was a method call becomes a network call. Apply resilience-patterns in
full — timeout, retry policy, breaker, fallback — and api-contracts to the interface, which is
now a published contract. A "pure refactor" that turns an in-process call into an unguarded remote
one has introduced a new outage mode.
Step 5 — Verify and close
- Run the whole suite, not only the tests near the change.
- Confirm the characterisation tests still pass unchanged. If any needed editing, behaviour
changed — say so explicitly rather than quietly adjusting the test.
- Re-read the diff as a reviewer. Anything outside the agreed scope → revert it.
- Check the target was actually reached. If the structure is only partly improved, say which
part remains and why, rather than implying it is done.
- Summarise: what moved, what did not change, which steps were taken, what is deliberately
left, and any bug found and deferred during characterisation.
Rules
- Behaviour is preserved, always. A behaviour change makes this a different workflow.
- Never refactor on red. Green before, green between, green after.
- Revert, do not debug forward when a step breaks tests.
- A bug found is a bug reported, not a bug fixed mid-refactor.
- Stay inside the agreed scope. Improvements you notice elsewhere go in the close-out list.
- No named force, no refactor. Say so plainly rather than producing churn.
1---2name: refactor-safely3description: Change the structure of existing code without changing its behaviour, in verified steps small enough to abandon. Covers pinning current behaviour with characterisation tests before touching anything, sequencing small reversible transformations, keeping the build green throughout, separating refactoring from behaviour change, and extracting a service from a monolith by strangler fig. Use when improving structure, paying down design debt, preparing code for a feature, splitting a module or service, or when the user says 'refactor', 'clean this up', 'restructure', 'extract', 'strangle', or 'reduce coupling'. This workflow preserves behaviour — to add or change behaviour use implement-service, to audit without changing use review-service.4license: MIT5---67# Refactor Safely89Refactoring is behaviour-preserving by definition. The moment behaviour changes, this stops being a10refactor and becomes a change that needs a design and a review.1112Two rules make the difference between a refactor and an outage: **pin the behaviour before you13touch it**, and **keep every step small enough to throw away**. A large refactor that "works" but14cannot be reverted is a rewrite wearing a safer name.1516## Required Skills1718Read and apply:19201. `test-strategy` — characterisation tests, and the right level to pin behaviour at. (always)212. `clean-architecture` — the target structure and why it is better. (always)223. `collaborative-judgment` — the scope, the target, and anything that turns out to be a behaviour23 change. (always)244. `service-boundaries` — module or service extraction. (conditional: moving code across a boundary)255. `domain-modelling` — restructuring domain types and aggregate boundaries. (conditional)266. `data-access` — schema changes, migrations, query restructuring. (conditional)277. `infrastructure-adapters` — introducing or reshaping a port to create a seam. (conditional)288. `api-contracts` — anything a consumer can observe. (conditional: a public surface is involved)299. `resilience-patterns` — moving code across a process boundary turns a call into a network hop.30 (conditional: extraction)3132## Workflow3334### Step 1 — Name the target and the reason3536Before any change, state three things and get agreement:37381. **What is wrong now** — the concrete pain, not an aesthetic judgment. "Adding a payment method39 requires edits in six files" is a reason. "This does not follow SOLID" is not.402. **What it should look like** — the target structure, in terms of `clean-architecture`.413. **What will be measurably better** — fewer files touched per change, a testable unit, a boundary42 the compiler enforces.4344If you cannot fill all three, **STOP**. Refactoring without a named force is churn: it produces a45large diff, a review burden, and a merge conflict for everyone else, in exchange for nothing.4647Agree the **scope boundary** explicitly — which files may change, and which are out of bounds. A48refactor that spreads is one nobody can review.4950### Step 2 — Pin the current behaviour5152You cannot preserve behaviour you have not captured.53541. **Run the existing tests.** They must be green before you start. Failing tests → fix or55 quarantine them first; you cannot tell your breakage from theirs otherwise.562. **Assess the coverage of the code you are about to move.** Read the tests; do not trust a57 percentage (`test-strategy`).583. **Write characterisation tests for what is not covered.** These pin what the code *does*, not59 what it should do:60 - Drive from the outside — the level where behaviour is observable without knowing the61 structure you are about to change. Usually a component test.62 - Include the ugly cases: nulls, empty collections, boundary values, error paths.63 - **If a characterisation test reveals a bug, do not fix it now.** Pin the current behaviour,64 note the bug, and raise it separately. Fixing behaviour during a refactor makes the diff65 impossible to reason about.664. **STOP** if the code cannot be characterised at any level. That is itself the finding — say so,67 and propose either adding a seam first or leaving it alone.6869### Step 3 — Transform in small steps7071Each step is one transformation, verified, and independently revertible.7273For every step:74751. Make **one** change — rename, extract, inline, move, introduce a parameter object, replace a76 conditional with polymorphism.772. **Run the tests.** Green → continue. Red → revert the step; do not debug forward. A revert costs78 seconds; debugging a half-applied refactor costs an afternoon.793. **Commit** at each green point, so any step can be abandoned without losing the rest.8081Rules that hold throughout:8283- **Never mix behaviour change with structure change.** If a fix or improvement is needed, finish84 the refactor, then make it as a separate change. If it cannot wait, stop the refactor, make the85 behaviour change with its own tests, then resume.86- **Keep the build green between steps.** Long-lived broken states are how refactors get abandoned87 half-done, leaving the codebase worse than before.88- **Prefer parallel-change (expand → migrate → contract)** for anything with callers: add the new89 shape, move callers over one at a time, remove the old one. Each stage ships independently.90- **Use the tooling.** IDE and language-server refactorings are safer than hand editing for renames91 and moves — but re-read the diff; automated moves miss reflection, strings, configuration, and92 documentation.93- **Update tests only for structure.** A test whose *assertions* must change means behaviour94 changed — stop and reconsider.9596Apply the `clean-architecture` checklist after each significant step, and surface any97`Ambiguity Signals` through `collaborative-judgment` rather than deciding the target unilaterally.9899### Step 4 — Extraction (module or service)100101Moving code across a process boundary is not a refactor of one codebase; it is a change to the102system's failure model. Follow the strangler-fig sequence in `service-boundaries`, and stop at each103stage:1041051. **Module first.** Move the code inside the existing deployable, behind an explicit interface.106 Add an architecture test that fails the build on a boundary violation. **Ship this.** Most of107 the risk lives here, and it is still cheap to undo.1082. **Separate the data.** Route every access through the interface; nothing else touches those109 tables. **Ship this.** If this proves impossible, the boundary was wrong — that is the cheapest110 possible moment to learn it.1113. **Add the remote implementation** of the same interface, called by nothing yet.1124. **Shift traffic behind a flag** (`config-and-dependencies`), comparing results, with an instant113 rollback.1145. **Remove the local implementation** only after the remote one has carried full traffic long115 enough to trust.116117At step 3, every call that was a method call becomes a network call. Apply `resilience-patterns` in118full — timeout, retry policy, breaker, fallback — and `api-contracts` to the interface, which is119now a published contract. A "pure refactor" that turns an in-process call into an unguarded remote120one has introduced a new outage mode.121122### Step 5 — Verify and close1231241. **Run the whole suite**, not only the tests near the change.1252. **Confirm the characterisation tests still pass unchanged.** If any needed editing, behaviour126 changed — say so explicitly rather than quietly adjusting the test.1273. **Re-read the diff as a reviewer.** Anything outside the agreed scope → revert it.1284. **Check the target was actually reached.** If the structure is only partly improved, say which129 part remains and why, rather than implying it is done.1305. **Summarise:** what moved, what did not change, which steps were taken, what is deliberately131 left, and any bug found and deferred during characterisation.132133## Rules134135- **Behaviour is preserved, always.** A behaviour change makes this a different workflow.136- **Never refactor on red.** Green before, green between, green after.137- **Revert, do not debug forward** when a step breaks tests.138- **A bug found is a bug reported**, not a bug fixed mid-refactor.139- **Stay inside the agreed scope.** Improvements you notice elsewhere go in the close-out list.140- **No named force, no refactor.** Say so plainly rather than producing churn.