Refactoring Golang with Claude Code: Safe and Structured, with a Safety Net
Refactor Golang code with Claude Code safely and in a structured way. Use spec and tests as a safety net for confident refactoring on Santekno Shop.
Refactoring with Claude: Safe, Structured, with a Safety Net
Refactoring is the art of changing code structure without changing its behavior. The key phrase is without changing behavior β that is what separates refactoring from rewriting, and what makes safe refactoring in Golang risky when done without a proper safety net. In SDD you have two strong nets: the spec defines the expected behavior, and the tests verify it. With both in place, Claude Code can execute a refactor efficiently while you keep confidence that nothing changed underneath.
15.1 Why Refactoring Is Risky (and How to Mitigate)
The main hazard is silent regression β behavior that shifts without anyone noticing, because no test catches it. A single operator flip is enough, as the comment-level diff below shows.
1// BEFORE: time.Since(CreatedAt) <= 15*time.Minute (inclusive β CORRECT, this is AC4)
2// AFTER: time.Since(CreatedAt) < 15*time.Minute (exclusive β subtle bug!)
3// At exactly 15 minutes: was cancellable, now isn't β an AC4 violationThe <= is not a stylistic choice: AC4 states the 15-minute boundary is INCLUSIVE β an order exactly 15 minutes 00 seconds old can still be cancelled, and the window only closes from 15 minutes 1 second onward. So the “before” version is the correct one, and flipping it to < is a bug that rejects a cancellation the spec still considers valid.
Without a test covering the “exactly 15 minutes” edge case, that bug ships undetected β which is precisely why the SDD safety net exists: AC4 already decides inclusive vs exclusive, a test written from the spec pins the boundary (exactly 15 minutes still cancellable; 15 minutes 1 second expired, per AC10), and the refactor runs with tests green after every change.
15.2 Three Refactoring Categories
Not all refactors carry the same risk, so classify before you start. The three categories below map directly to how much human oversight a change needs.
- Category 1 β Code Structure (extract method, rename, move code): Low risk.
- Category 2 β Pattern Refactoring (change error-handling style, struct-to-composition): Medium risk.
- Category 3 β Algorithmic Refactoring (optimize a query, change a concurrency pattern): High risk.
The rule that follows is simple: the higher the category, the more you lean on exhaustive tests and human review rather than trusting an AI edit blindly.
15.3 Safety Net: Test Coverage Before Refactoring
Before touching anything, measure coverage on the file you intend to change so you know how much protection you actually have. The commands below capture a baseline.
1go test -race -cover -coverprofile=before-refactor.out ./...
2go tool cover -func=before-refactor.out | grep create_order
3# Verify: 80%+ before refactoringThe practical rule this enforces: never refactor a file below 80% coverage without adding tests first β otherwise you are walking a tightrope with no net.
15.4 Extract Method Refactoring
The most common Category 1 change is extracting a block into its own method. The prompt below is precise about the one thing that matters β behavior must not change.
1Refactor CreateOrderUseCase.Execute by extracting stock validation
2into a separate method.
3
4Requirements:
51. New method: validateCartItems(ctx, items []CartItem) error
62. Behavior MUST NOT change β return the same errors for all cases
73. All existing tests must pass without modification
84. Follow the existing error handling style
9
10Constraints:
11- Don't change the Execute signature
12- Don't change existing error types
13- Only extract, don't add or change logic
14
15After refactoring: run go test -race ./internal/usecase/order/...
16Confirm all tests pass.The clause “all existing tests must pass without modification” is the safety valve β if a test needs editing to stay green, the refactor changed behavior and must be reconsidered, not the test.
15.5 Standardize Error Handling Pattern
A Category 2 pattern refactor unifies inconsistent conventions across files. Here the goal is one error-wrapping style everywhere, with no behavioral change.
1I found inconsistency in error handling across usecase files:
2
3File A: return nil, fmt.Errorf("create order: get cart: %w", err)
4File B: return fmt.Errorf("get order: %w", err) // missing usecase prefix
5File C: return nil, err // no wrapping at all
6
7Apply a consistent convention: "[usecase_action]: [operation]: %w"
8Example: "cancel order: get order: %w"
9
10Do NOT change: error types, control flow, return values
11Only change: error message stringsBecause only the message strings move, the risk is low β but the “do not change error types or control flow” guardrail is what keeps a cosmetic refactor from quietly altering which errors callers can match.
15.6 Interface Extraction Refactoring
Swapping a concrete dependency for an interface improves testability and flexibility, but it touches construction and wiring. The prompt spells out each step so nothing is missed.
1CancelOrderUseCase currently depends on *kafka.Producer (concrete).
2Refactor to depend on an interface for testability and flexibility.
3
4Steps:
51. Define an EventPublisher interface in internal/domain/event/publisher.go
62. Update CancelOrderUseCase to depend on the interface
73. Create a KafkaEventPublisher wrapper struct
84. Update dependency injection in main.go
9
10Behavior MUST NOT change.
11Existing tests MUST pass.The reward is a usecase you can mock in unit tests without a real Kafka β but note the change spans production wiring, so it belongs in its own PR where the diff is easy to reason about.
15.7 Verification After Refactoring
A refactor is not done when the code compiles β it is done when a battery of checks confirms nothing regressed. Run all five after every change.
1# Coverage must not decrease
2go test -race -cover -coverprofile=after-refactor.out ./...
3
4# No new race conditions
5go test -race ./...
6
7# No regression (run 3 times to catch flaky tests)
8go test ./... -count=3
9
10# Performance not regressed (for algorithmic refactoring)
11go test -bench=. -benchmem ./internal/usecase/order/...
12
13# Static analysis still clean
14golangci-lint run ./internal/usecase/order/...The -count=3 run is the underrated one β a single green pass can hide a flaky test that a refactor just made non-deterministic, and three passes surface it before your users do.
15.8 Incremental Refactoring Strategy
Large refactors fail when attempted in one heroic commit, so break them into phases. The plan below splits an oversized usecase one extraction at a time.
1Large refactoring: split a 300-line CreateOrderUseCase into smaller focused usecases.
2
3DON'T do it all at once. Do it incrementally:
4
5Phase 1 (this week): Extract validateCartItems
6Phase 2 (next week): Extract calculateOrderTotal
7Phase 3 (2 weeks): Extract createOrderRecord
8
9For Phase 1 only, right now:
10- One commit per phase
11- Tests must pass on every commit
12- One PR per phase (not all in one PR)One phase per PR keeps every diff reviewable and every step reversible β if Phase 2 introduces a regression, you revert one small commit instead of unpicking a thousand-line change.
15.9 Strangler Fig Pattern for Large Refactoring
For a high-risk migration you cannot do in one shot, run the old and new implementations side by side and shift traffic gradually. The Strangler Fig plan below does exactly that.
1Large refactoring: migrate OrderRepository from raw pgx to a new pattern.
2
3Use the Strangler Fig approach:
41. Create a new OrderRepository interface
52. Create a new implementation in internal/repository/v2/postgres/
63. Run BOTH in parallel behind a feature flag
74. Move one endpoint to the new implementation
85. Monitor in production
96. If healthy, move endpoints one by one
107. Delete the old implementation once all are migrated
11
12Create a plan for Phase 1 only (interface + new implementation).The feature flag is the safety mechanism β production traffic proves the new path before you commit to it, so a bad migration is a flag flip away from rollback rather than an emergency revert.
15.10 Test-First Refactoring
When a refactor is genuinely risky, write the tests before you touch the code. The sequence below is TDD applied to refactoring.
1Before refactoring CanBeCancelled() for a new business rule:
21. Write tests verifying the existing behavior (regression protection)
32. Write tests for the new behavior (they will fail initially)
43. Refactor until both test sets pass
5
6This is TDD applied to refactoring.Writing the regression tests first means you cannot silently change old behavior β step 1 locks it in, and step 2 makes the new requirement explicit and verifiable before any code moves.
15.11 Refactoring for Testability
Sometimes code is hard to test because of a hidden dependency on wall-clock time. Injecting a clock interface makes the 15-minute window deterministic under test.
1type Clock interface {
2 Now() time.Time
3}The plan around this interface is: define Clock, add a RealClock production implementation, thread it through the CancelOrderUseCase constructor, decide whether CanBeCancelled takes the clock or stays in the usecase, then update the tests. The result is a window check you can freeze at any instant β no more flaky tests that depend on when they happened to run.
15.12 Batch Refactoring
Applying one identical change across many files is where AI shines, provided the instruction is unambiguous. The prompt below performs a mechanical, repo-wide substitution.
1Apply the same change to all usecase files:
2
3REPLACE:
4slog.Error("something failed", "err", err)
5
6WITH:
7slog.ErrorContext(ctx, "something failed", "err", err)
8
9Files: all files in internal/usecase/order/
10All functions already have a ctx parameter.
11Don't change anything else.
12After: go build ./...The line “all functions already have a ctx parameter” is doing quiet but critical work β it is the precondition that makes the substitution safe, and stating it prevents the model from inventing a context where none exists.
15.13 Refactoring and Spec Updates
Occasionally a refactor exposes a gap in the spec itself. When that happens, fix the spec before the code β the prompt below enforces that order.
1While refactoring CanBeCancelled, I found the spec didn't yet define
2"exactly at 15 minutes" β inclusive or exclusive?
3
4Current code uses <= (inclusive).
5
6Action plan:
71. Check the spec for any definition
82. If missing, add a clarification to the spec
93. Write an explicit test for the boundary case
104. Ensure the implementation matches the spec clarification
11
12Don't refactor the code until the spec clarification exists.That clarification became AC4: the boundary is inclusive, so time.Since(CreatedAt) <= 15*time.Minute is not an accident of how the code happened to be written β it is the direct implementation of AC4, and changing <= to < is now a spec violation rather than a matter of taste (see Β§15.1).
Refusing to refactor until the spec is clarified keeps the spec as the source of truth β otherwise the refactor would quietly decide the ambiguous behavior instead of implementing a documented one.
15.14 What AI Refactoring Is Good and Bad For
Matching the tool to the task keeps you out of trouble. AI is very good at mechanical, well-bounded work and weaker at judgment-heavy changes:
- Very good via AI: extract method/function, rename for consistency, standardize patterns, format and style consistency, simple query optimization.
- Better as human-led: significant architectural restructuring, business logic with subtle correctness requirements, algorithmic refactoring that needs a real correctness proof.
The dividing line is judgment β where the change is mechanical, let the AI drive; where it requires understanding why the business rules are what they are, keep a human in the seat.
15.15 Good Commit Messages for Refactoring
A refactor commit should explain intent and prove safety, because the diff alone cannot. Contrast the two examples below.
1# Good
2git commit -m "refactor(order): extract validateCartItems from Execute
3
4Extract stock validation logic into a separate private method.
5Motivation: improve readability and enable independent unit testing.
6Behavior: identical to pre-extraction code.
7Coverage: maintained at 88.4% (unchanged)
8Tests: all 7 test cases pass"
9
10# Bad
11git commit -m "refactor some stuff"
12git commit -m "cleanup"The good message records the two facts a future reviewer needs β that behavior is unchanged and coverage held β turning the commit itself into part of the safety net.
15.16 Refactoring Documentation Pattern
For non-trivial refactors, keep a short log so the team can trace what changed and why. The entry format below captures type, before/after, and behavior impact.
1## Refactoring Log
2
3### 2025-07-01: Extract validateCartItems
4Type: Extract Method (Category 1 β Code Structure)
5Before: 85-line Execute method
6After: Execute + validateCartItems (separated concerns)
7Behavior change: None
8Test coverage: 88.4% -> 88.4% (no change)
9Motivation: Improve readability, enable independent testingAn entry like this makes “Behavior change: None” an explicit, reviewable claim rather than an assumption β and the coverage delta beside it is the evidence backing that claim.
15.17 Spec Compliance After Refactoring
Even a behavior-preserving refactor deserves a final compliance pass, because “I didn’t mean to change behavior” is not proof. The prompt below re-verifies the spec.
1Just completed a refactoring of CreateOrderUseCase.Execute
2(extracted validateCartItems and standardized error handling).
3
4Verify that spec compliance is maintained:
5Spec: specs/order/create-order.md v1.2
6
7For each AC, verify the refactored code still implements it correctly.
8Any regressions?Closing the loop back to the spec is what makes SDD refactoring trustworthy β the same document that authorized the behavior now certifies the refactor preserved it.
15.18 Tips & Gotchas
The disciplines below are what keep AI-assisted refactoring on the safe side of the line:
Tip 1: Test after each small change β don’t wait until the whole refactor is done.
Tip 2: Review git diff before committing β make sure nothing unintended slipped in.
Tip 3: Keep refactoring and new features in separate PRs β don’t mix them.
Tip 4: Document the motivation β “extracted for readability” in the commit message.
Gotcha 1: AI can introduce subtle behavior changes β always run tests with -race afterward.
Gotcha 2: Large refactoring PRs can’t be reviewed effectively β keep them small and focused.
Gotcha 3: Don’t refactor code without tests β write tests first, then refactor.
Gotcha 4: Performance refactoring needs benchmarks β don’t assume “more efficient,” measure it.
15.19 The Refactoring Discipline
Underneath every technique is a small set of non-negotiable disciplines. The five below are what actually make refactoring safe:
- Never refactor without tests β spec + tests = safety net.
- Small steps β one extraction, one rename, one pattern at a time.
- Test after each step β don’t accumulate untested changes.
- Separate PR β don’t mix refactoring with features.
- Document motivation β explain the why, not just the what.
Claude Code accelerates the execution of these disciplines β but the discipline itself has to come from the developer, not the tool.
15.20 Summary
Refactoring with Claude Code is powerful but demands discipline. The safety net of spec (defines expected behavior) plus tests (verifies it) is fundamental to doing it safely.
Three risk categories: code structure (low) β pattern refactoring (medium) β algorithmic refactoring (high). Higher risk means more human oversight.
Safe workflow: verify coverage β add missing tests β refactor incrementally β test after every step β review the diff before commit.
AI is great for: extract method, rename consistency, standardizing patterns. It is less suited to large architectural refactoring and business-logic changes.
Never refactor without tests β this is non-negotiable. Coverage below 80% means adding tests first, then refactoring.
This is the last article in Part 3 of the series. In the next article we enter Part 4: SDD at Team Scale β beginning with how SDD works in a microservice environment with API contracts between services.