Refactor
Behavior-preserving restructuring, Fowler-style. The catalog below is the vocabulary; the
workflow is what keeps it safe.
Workflow
- Pin behavior first. Find the existing test class
(
<module>/src/test/java/<mirrored package>/<ClassName>Test.java). If the code you're about to
move has no test covering it, write a characterization test against the current behavior
before touching anything. If the code is untestable as-is, the first refactoring is the one
that makes it testable (usually Extract Method / Extract Class), done in the smallest step you
can verify by compiling.
- One refactoring per step. Apply a single named transformation, then compile. Never mix two
catalog entries in one edit, and never mix a refactoring with a behavior change — if you spot a
bug mid-refactor, note it and finish the refactor first, then fix it as a separate change with
its own failing test.
- Run the affected tests after each step, not just at the end, and delegate the run to the
test-runner agent (CLAUDE.md) instead of hand-rolling Maven — it picks the runner for what
you touched (a core class or package, a distribution/tutorial IT, or a module-wide run for
annot; war has no tests of its own) and knows the traps that make a hand-rolled command
silently run the wrong scope. Green after every step is the safety net for behavior, but a
sequential test run does not establish thread safety: a refactoring that changes what is
shared between threads needs a targeted concurrent test (below).
- Report at the end: which refactorings were applied to which methods, what is now testable
that wasn't, whether anything you moved changed what is shared between threads, and anything
you deliberately left alone.
Scope discipline
- Refactor only what the user named plus what that change strictly requires. Adjacent messy code
stays messy — mention it, don't touch it (CLAUDE.md §3).
- Public API of
@MCElement-annotated config classes is a contract: attribute/child setter names
and signatures are the config grammar. Do not rename or re-sign them as part of a refactor.
Private helpers behind them are fair game.
- Delete what your change orphaned (now-unused imports, fields, private methods). Leave
pre-existing dead code alone.
Thread safety
One interceptor instance serves every request thread (Interceptor javadoc: "Interceptor
implementations need to be thread safe"), and <call>/internal routing re-enters that same
instance on its own thread — so per-request state in a field is corrupted by nesting before it is
ever raced on. A refactoring that moves state out of a local and onto the object therefore changes
behavior invisibly: parallel execution is commented out in
core/src/test/resources/junit-platform.properties, so every unit test runs single-threaded and
an ordinary sequential test run does not establish thread safety. Catch it by reading the diff, and
when a refactoring changes what is shared between threads, cover it with a targeted concurrent test
(drive the refactored code from several threads — ExecutorService plus a CountDownLatch to
start them together — and assert each thread's own result).
Preserve what is shared. Locals, parameters and return values are per-request; fields on an
interceptor are per-server. Never convert the first into the second to shorten a signature. Real
per-request state goes on the Exchange (ProtocolHandler javadoc), never into a field — see the
standing reminder at REST2SOAPInterceptor.java:183, "Determine SOAP version per-request; do not
cache in instance state".
Scan your own diff for these, each a race until you can argue otherwise:
- A new field on an interceptor that a request path writes.
- Extract Class whose result becomes a field. Anything holding per-message state is built per
call —
XMLProtector wraps one message's writer. Only a stateless or immutable helper may be
held as a field.
- Lazy initialization or a cache introduced by Replace Temp with Query. Config-derived values
are computed in
init(), which is guaranteed to run before any port opens — prefer that (the
config-error-handling skill), and keep it idempotent: RuleReinitializer can re-run it.
static mutable state. static final is safe for primitives, String and List.of(...);
it is a race for SimpleDateFormat, Matcher, MessageDigest, DocumentBuilder, Transformer
and the StAX/DOM/XPath factories. The repo's three sanctioned answers: build it per call, a
static final ThreadLocal (HardenedStaxInputFactory, XPathUtil), or a pool
(XSLTTransformer).
- Deleting
volatile, synchronized, Atomic*, ThreadLocal or a concurrent collection
because it reads as redundant — assume it is load-bearing until git log -S says otherwise.
- Handing a body or an
Exchange to another thread — AbstractBody: "Accessing the body from
multiple threads is illegal."
final and CLAUDE.md's immutable-by-default rule are doing concurrency work here, not just style
work: a final field is safely published, a record parameter object is shareable by
construction. A race you find is a bug, not a refactoring — note it and fix it separately
(step 2).
Catalog
Extract Method — the default move
Pull out any block that has one responsibility and could be tested on its own. Signals: a comment
explaining what the next few lines do, a blank-line-separated paragraph inside a method, a loop
body doing real work, a nested conditional branch of more than ~3 lines.
- Name the method after what it answers or produces, not how —
isExpiredToken,
resolveSchemaFor, not doCheck2.
- If the block reads three fields and writes none, pass them as parameters; if it writes two or
more locals, the block wants Extract Class instead (below), not a method with out-params. Either
way the extracted method takes what it needs as parameters — a new field to dodge a parameter is
shared state (see Thread safety).
- Keep the extracted method small and cohesive; a helper that itself needs a section comment is
not done being extracted.
Replace Temp with Query / Inline Variable
A local that is assigned once from an expression and read later is usually a name looking for a
method. Replace it with a call to a small query method — that removes the temp and makes the
computation reachable from a test.
- Only when the expression is pure. A query is evaluated at every read, so this is not
behavior-preserving if the expression has side effects, observes mutable state that changes in
between, allocates an object whose identity is compared, or is expensive. In those cases keep the
local (call the helper once and assign it) instead. On a shared interceptor "changes in between"
includes another thread changing it, so a query reading a mutable field is not pure.
- Inline a temp outright only when the expression is short
- A variable that gets reassigned is the strongest extract signal in this repo: CLAUDE.md says
reassignment means "extract a helper method instead". Convert accumulate-in-a-loop temps into a
helper returning the value, and make the remaining locals
final.
- Split Temporary Variable when one local holds two different things at two points; never reuse a
name for a second purpose.
Remove Duplication
Duplication first, abstraction second — extract the shared code only once you've seen it twice and
the two copies mean the same thing (same reason to change), not merely look alike.
- Identical code in two methods of one class → Extract Method, call it from both.
- Identical code in two sibling subclasses → Pull Up Method to the shared supertype.
- Same shape, different values → parameterize the extracted method. Same shape, different step
→ pass the varying step as a small functional interface or use Template Method — but only if
there are ≥3 call sites, otherwise the indirection costs more than the duplication.
- Similar-looking code that would need to change for different reasons is not duplication.
Leave it and say so.
Introduce Abstractions — only when they pay
CLAUDE.md forbids speculative abstraction. Introduce one only when a concrete pressure exists now:
- Extract Class — a class holding two clusters of fields that don't talk to each other, or a
method needing 4+ locals to survive extraction. Move the cluster and the methods that use it.
Decide the new object's lifetime explicitly: per call (the default for anything stateful) or a
field (only if stateless or immutable).
- Replace Conditional with Polymorphism / sealed switch — a
switch or if/else chain on a
type tag repeated in more than one method. This repo prefers pattern matching over sealed types
and records to hand-rolled hierarchies (CLAUDE.md code style) — prefer a switch over a sealed
interface before inventing an abstract base class.
- Introduce Parameter Object / Value Object — see long parameter lists below.
- Replace Magic Literal with Constant — a literal appearing twice, or once with non-obvious
meaning.
static final only for immutable values: a shared SimpleDateFormat or Matcher is a
race, not a constant.
- Do not introduce an interface with a single implementation, a factory for a constructor
call, or a strategy for a two-branch conditional.
When you think an abstraction is worth it, ask.
Move Method — feature envy
A method that calls more methods/fields of another object than of its own belongs on that other
object. Move it, leaving a delegating method behind only if external callers need it.
- Extract the envious part first if only a portion is envious, then move that.
- Related smells: Move Field (a field used mainly by another class), Hide Delegate (callers doing
a.getB().getC().doIt() — give a the method), Remove Middle Man (a class that only forwards).
- If the target class is one you must not change (a JDK/library type or a generated/config class),
don't move — extract a static helper in a
util-style class instead and note why.
Long Parameter Lists
Threshold: more than 3 parameters, or any two adjacent parameters of the same type (call-site
transposition bugs).
- Parameters that always travel together and mean one thing → Introduce Parameter Object, as a
record (immutable, matches the repo's immutable-by-default rule).
- A parameter derivable from another parameter → Replace Parameter with Query, drop it.
- A boolean flag that selects behavior → Remove Flag Argument: split into two clearly named
methods.
- Several parameters that are all fields of the caller → the method probably wants to move to the
caller's class (feature envy, above).
- Promoting parameters to fields is not a way to shorten a signature — on an interceptor that
turns per-request state into per-server state (see Thread safety).
- Don't abbreviate parameter names in public interfaces (
docs/CONVENTIONS.md).
Repo-specific constraints
- Java 21. Prefer
records for parameter/value objects, pattern-matching switch over sealed
types, List.of/Collections.unmodifiableList for extracted collection state,
getFirst() over .get(0) — but note the swap changes the empty-list exception from
IndexOutOfBoundsException to NoSuchElementException, so only do it where the list is
guaranteed non-empty or the exception type isn't part of the contract (no test or caller
catches it).
final on extracted fields, parameters, and locals wherever it compiles.
- SLF4J only; never introduce
System.out while moving code.
- Don't reformat lines you didn't otherwise change — a diff full of whitespace hides the real
refactoring.
- Every newly extracted method that has real behavior needs at least one test; add it to the
existing mirrored test class rather than creating a new one (CLAUDE.md testing rules). The
exemption covers only plain accessors — a bare field read/write,
record components, generated
equals/hashCode/toString. An extracted accessor that validates, lazily initializes,
computes, logs, or does I/O has behavior and needs a test.
When to stop
Stop when the named smell is gone. Resist the pull to keep going: a refactor that touches twice
the files the user expected is a worse outcome than one that leaves a second smell for later.
Name the leftovers in your report instead of fixing them.
1---2name: refactor3description: Refactor Java code in this repo without changing behavior. Not for bug fixes or new features.4---56# Refactor78Behavior-preserving restructuring, Fowler-style. The catalog below is the vocabulary; the9workflow is what keeps it safe.1011## Workflow12131. **Pin behavior first.** Find the existing test class14 (`<module>/src/test/java/<mirrored package>/<ClassName>Test.java`). If the code you're about to15 move has no test covering it, write a characterization test against the *current* behavior16 before touching anything. If the code is untestable as-is, the first refactoring is the one17 that makes it testable (usually Extract Method / Extract Class), done in the smallest step you18 can verify by compiling.192. **One refactoring per step.** Apply a single named transformation, then compile. Never mix two20 catalog entries in one edit, and never mix a refactoring with a behavior change — if you spot a21 bug mid-refactor, note it and finish the refactor first, then fix it as a separate change with22 its own failing test.233. **Run the affected tests** after each step, not just at the end, and delegate the run to the24 `test-runner` agent (CLAUDE.md) instead of hand-rolling Maven — it picks the runner for what25 you touched (a `core` class or package, a distribution/tutorial IT, or a module-wide run for26 `annot`; `war` has no tests of its own) and knows the traps that make a hand-rolled command27 silently run the wrong scope. Green after every step is the safety net for behavior, but a28 sequential test run does not establish thread safety: a refactoring that changes what is29 shared between threads needs a targeted concurrent test (below).304. **Report** at the end: which refactorings were applied to which methods, what is now testable31 that wasn't, whether anything you moved changed what is shared between threads, and anything32 you deliberately left alone.3334## Scope discipline3536- Refactor only what the user named plus what that change strictly requires. Adjacent messy code37 stays messy — mention it, don't touch it (CLAUDE.md §3).38- Public API of `@MCElement`-annotated config classes is a contract: attribute/child setter names39 and signatures are the config grammar. Do not rename or re-sign them as part of a refactor.40 Private helpers behind them are fair game.41- Delete what your change orphaned (now-unused imports, fields, private methods). Leave42 pre-existing dead code alone.4344## Thread safety4546One interceptor instance serves every request thread (`Interceptor` javadoc: "Interceptor47implementations need to be thread safe"), and `<call>`/internal routing re-enters that same48instance on its own thread — so per-request state in a field is corrupted by nesting before it is49ever raced on. A refactoring that moves state out of a local and onto the object therefore changes50behavior invisibly: parallel execution is commented out in51`core/src/test/resources/junit-platform.properties`, so every unit test runs single-threaded and52an ordinary sequential test run does not establish thread safety. Catch it by reading the diff, and53when a refactoring changes what is shared between threads, cover it with a targeted concurrent test54(drive the refactored code from several threads — `ExecutorService` plus a `CountDownLatch` to55start them together — and assert each thread's own result).5657**Preserve what is shared.** Locals, parameters and return values are per-request; fields on an58interceptor are per-server. Never convert the first into the second to shorten a signature. Real59per-request state goes on the `Exchange` (`ProtocolHandler` javadoc), never into a field — see the60standing reminder at `REST2SOAPInterceptor.java:183`, "Determine SOAP version per-request; do not61cache in instance state".6263Scan your own diff for these, each a race until you can argue otherwise:6465- **A new field** on an interceptor that a request path writes.66- **Extract Class whose result becomes a field.** Anything holding per-message state is built per67 call — `XMLProtector` wraps one message's writer. Only a stateless or immutable helper may be68 held as a field.69- **Lazy initialization or a cache** introduced by Replace Temp with Query. Config-derived values70 are computed in `init()`, which is guaranteed to run before any port opens — prefer that (the71 `config-error-handling` skill), and keep it idempotent: `RuleReinitializer` can re-run it.72- **`static` mutable state.** `static final` is safe for primitives, `String` and `List.of(...)`;73 it is a race for `SimpleDateFormat`, `Matcher`, `MessageDigest`, `DocumentBuilder`, `Transformer`74 and the StAX/DOM/XPath factories. The repo's three sanctioned answers: build it per call, a75 `static final ThreadLocal` (`HardenedStaxInputFactory`, `XPathUtil`), or a pool76 (`XSLTTransformer`).77- **Deleting `volatile`, `synchronized`, `Atomic*`, `ThreadLocal` or a concurrent collection**78 because it reads as redundant — assume it is load-bearing until `git log -S` says otherwise.79- **Handing a body or an `Exchange` to another thread** — `AbstractBody`: "Accessing the body from80 multiple threads is illegal."8182`final` and CLAUDE.md's immutable-by-default rule are doing concurrency work here, not just style83work: a `final` field is safely published, a `record` parameter object is shareable by84construction. A race you *find* is a bug, not a refactoring — note it and fix it separately85(step 2).8687## Catalog8889### Extract Method — the default move90Pull out any block that has one responsibility and could be tested on its own. Signals: a comment91explaining what the next few lines do, a blank-line-separated paragraph inside a method, a loop92body doing real work, a nested conditional branch of more than ~3 lines.9394- Name the method after **what it answers or produces**, not how — `isExpiredToken`,95 `resolveSchemaFor`, not `doCheck2`.96- If the block reads three fields and writes none, pass them as parameters; if it writes two or97 more locals, the block wants Extract Class instead (below), not a method with out-params. Either98 way the extracted method takes what it needs as parameters — a new field to dodge a parameter is99 shared state (see Thread safety).100- Keep the extracted method small and cohesive; a helper that itself needs a section comment is101 not done being extracted.102103### Replace Temp with Query / Inline Variable104A local that is assigned once from an expression and read later is usually a name looking for a105method. Replace it with a call to a small query method — that removes the temp *and* makes the106computation reachable from a test.107108- **Only when the expression is pure.** A query is evaluated at every read, so this is not109 behavior-preserving if the expression has side effects, observes mutable state that changes in110 between, allocates an object whose identity is compared, or is expensive. In those cases keep the111 local (call the helper once and assign it) instead. On a shared interceptor "changes in between"112 includes another thread changing it, so a query reading a mutable field is not pure.113- Inline a temp outright only when the expression is short114- A variable that gets **reassigned** is the strongest extract signal in this repo: CLAUDE.md says115 reassignment means "extract a helper method instead". Convert accumulate-in-a-loop temps into a116 helper returning the value, and make the remaining locals `final`.117- Split Temporary Variable when one local holds two different things at two points; never reuse a118 name for a second purpose.119120### Remove Duplication121Duplication first, abstraction second — extract the shared code only once you've seen it twice and122the two copies mean the same thing (same reason to change), not merely look alike.123124- Identical code in **two methods of one class** → Extract Method, call it from both.125- Identical code in **two sibling subclasses** → Pull Up Method to the shared supertype.126- Same shape, different values → parameterize the extracted method. Same shape, different *step*127 → pass the varying step as a small functional interface or use Template Method — but only if128 there are ≥3 call sites, otherwise the indirection costs more than the duplication.129- Similar-looking code that would need to change for different reasons is **not** duplication.130 Leave it and say so.131132### Introduce Abstractions — only when they pay133CLAUDE.md forbids speculative abstraction. Introduce one only when a concrete pressure exists now:134135- **Extract Class** — a class holding two clusters of fields that don't talk to each other, or a136 method needing 4+ locals to survive extraction. Move the cluster and the methods that use it.137 Decide the new object's lifetime explicitly: per call (the default for anything stateful) or a138 field (only if stateless or immutable).139- **Replace Conditional with Polymorphism / sealed switch** — a `switch` or `if/else` chain on a140 type tag repeated in more than one method. This repo prefers pattern matching over sealed types141 and records to hand-rolled hierarchies (CLAUDE.md code style) — prefer a `switch` over a sealed142 interface before inventing an abstract base class.143- **Introduce Parameter Object / Value Object** — see long parameter lists below.144- **Replace Magic Literal with Constant** — a literal appearing twice, or once with non-obvious145 meaning. `static final` only for immutable values: a shared `SimpleDateFormat` or `Matcher` is a146 race, not a constant.147- Do **not** introduce an interface with a single implementation, a factory for a constructor148 call, or a strategy for a two-branch conditional.149150When you think an abstraction is worth it, ask.151152### Move Method — feature envy153A method that calls more methods/fields of another object than of its own belongs on that other154object. Move it, leaving a delegating method behind only if external callers need it.155156- Extract the envious *part* first if only a portion is envious, then move that.157- Related smells: Move Field (a field used mainly by another class), Hide Delegate (callers doing158 `a.getB().getC().doIt()` — give `a` the method), Remove Middle Man (a class that only forwards).159- If the target class is one you must not change (a JDK/library type or a generated/config class),160 don't move — extract a static helper in a `util`-style class instead and note why.161162### Long Parameter Lists163Threshold: **more than 3 parameters, or any two adjacent parameters of the same type** (call-site164transposition bugs).165166- Parameters that always travel together and mean one thing → **Introduce Parameter Object**, as a167 `record` (immutable, matches the repo's immutable-by-default rule).168- A parameter derivable from another parameter → **Replace Parameter with Query**, drop it.169- A boolean flag that selects behavior → **Remove Flag Argument**: split into two clearly named170 methods.171- Several parameters that are all fields of the caller → the method probably wants to move to the172 caller's class (feature envy, above).173- Promoting parameters to fields is **not** a way to shorten a signature — on an interceptor that174 turns per-request state into per-server state (see Thread safety).175- Don't abbreviate parameter names in public interfaces (`docs/CONVENTIONS.md`).176177## Repo-specific constraints178179- Java 21. Prefer `record`s for parameter/value objects, pattern-matching `switch` over sealed180 types, `List.of`/`Collections.unmodifiableList` for extracted collection state,181 `getFirst()` over `.get(0)` — but note the swap changes the empty-list exception from182 `IndexOutOfBoundsException` to `NoSuchElementException`, so only do it where the list is183 guaranteed non-empty or the exception type isn't part of the contract (no test or caller184 catches it).185- `final` on extracted fields, parameters, and locals wherever it compiles.186- SLF4J only; never introduce `System.out` while moving code.187- Don't reformat lines you didn't otherwise change — a diff full of whitespace hides the real188 refactoring.189- Every newly extracted method that has real behavior needs at least one test; add it to the190 existing mirrored test class rather than creating a new one (CLAUDE.md testing rules). The191 exemption covers only *plain* accessors — a bare field read/write, `record` components, generated192 `equals`/`hashCode`/`toString`. An extracted accessor that validates, lazily initializes,193 computes, logs, or does I/O has behavior and needs a test.194195## When to stop196197Stop when the named smell is gone. Resist the pull to keep going: a refactor that touches twice198the files the user expected is a worse outcome than one that leaves a second smell for later.199Name the leftovers in your report instead of fixing them.200201