Skip to content
Santekno.com | Level Up Your Engineering Skills
EN
📖 0%
25 Sep 2026 · 24 min read ·Article 53 / 208
Go

Refactoring Golang with AI: A Strategy for Safe Large-Scale Changes

How to refactor Go safely and effectively with AI. From simple renames to extracting interfaces and migrating patterns at large scale with Claude Code and Cursor.

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

Refactoring with AI: Safe Large-Scale Changes

Refactoring Golang with AI turns one of the riskiest activities in software development into something far more controllable. Refactoring is so often deferred precisely because of its risk. AI changes this equation significantly: with an AI that can analyze impact comprehensively and execute changes across many files at once, a refactoring that used to take 2 days can finish in 2 hours.

What you need to protect is not the speed but the discipline. Throughout this article we will discuss how to refactor with AI while staying safe: a safety net, incremental steps, and verification at every stage.


13.1 Refactoring Types and the Best Tool for Each Type

Not all refactorings carry the same risk, and the right tool differs for each type. The map below groups refactorings from the most local to those that touch the entire architecture, complete with tool recommendations and strategies.

text
 1TYPE 1: Local Refactoring (within a single function/file)
 2  Example: Simplify a complex condition, extract a helper method
 3  Best tool: Cursor inline or Copilot inline
 4  Risk: LOW — easy to verify with go test
 5  Strategy: Do it directly, verify with test
 6
 7TYPE 2: Package-level Refactoring (within a single package)
 8  Example: Extract interface, reorganize structs, rename types
 9  Best tool: Claude Code or Cursor Composer
10  Risk: MEDIUM — can affect tests and callers in other packages
11  Strategy: Tests first, refactor, verify
12
13TYPE 3: Cross-package Refactoring
14  Example: Move a struct to a new package, change interface signatures
15  Best tool: Claude Code (reasoning) or Gemini (large codebase)
16  Risk: HIGH — compilation errors in many places
17  Strategy: Plan first, batch changes, verify every step
18
19TYPE 4: Architecture Refactoring
20  Example: Migrate from monolith to layered, extract domain entities
21  Best tool: Claude Code (most reliable for reasoning)
22  Risk: VERY HIGH — affects the entire codebase
23  Strategy: Incremental over multiple PRs, extensive testing

The further down the table, the greater the risk and the more planning it needs — do not treat an architecture refactoring with the same discipline you would apply to extracting a helper.


13.2 Safety Net: Setup Before Refactoring

Before touching a single line of code, prepare a safety net so that every change can be measured and reverted. The following sequence of commands must be run before any refactoring, and compared again afterward.

bash
 1# MANDATORY before any refactoring:
 2
 3# 1. Commit the working state
 4git status  # make sure it's clean or stash
 5git checkout -b refactor/cancel-order-interface
 6git add -A && git commit -m "checkpoint: pre-refactoring"
 7
 8# 2. Verify all tests pass
 9go test -race ./...  # must be 100% pass
10
11# 3. Coverage baseline
12go test -coverprofile=pre-refactor.out ./...
13go tool cover -func=pre-refactor.out > pre-refactor-coverage.txt
14
15# 4. Verify the build
16go build ./...
17
18# 5. Verify vet
19go vet ./...
20
21# After refactoring, compare:
22go test -coverprofile=post-refactor.out ./...
23go tool cover -func=post-refactor.out > post-refactor-coverage.txt
24diff pre-refactor-coverage.txt post-refactor-coverage.txt
25# No file's coverage should drop

With a coverage baseline and a checkpoint commit, you always have an objective reference point: if coverage drops or a test goes red, you know exactly where to return.


13.3 Extract Interface Refactoring

One of the most common refactorings in Go Clean Architecture is breaking up an interface that has grown too fat. The prompt below instructs Claude Code to split OrderRepository into two smaller interfaces while preserving backward compatibility.

bash
 1# Scenario: the OrderRepository interface is now too fat
 2# Need to extract into: ReadOrderRepository + WriteOrderRepository
 3
 4# Prompt to Claude Code:
 5claude
 6> Refactor the OrderRepository interface in internal/domain/order/repository.go
 7> into two smaller interfaces (Interface Segregation):
 8>
 9> ReadOrderRepository:
10>   GetByIDAndUserID, GetByID, ListByUserID, CountByStatus
11>
12> WriteOrderRepository:
13>   Create, CancelWithStockRestore, UpdateStatus, SoftDelete
14>
15> Requirements:
16> 1. Create two new interfaces in the same file
17> 2. Create an OrderRepository interface that embeds both (backward compat)
18> 3. Update all usecases that currently use OrderRepository:
19>    - cancel_order.go: only needs ReadOrderRepository + WriteOrderRepository
20>    - list_orders.go: only needs ReadOrderRepository
21>    - create_order.go: only needs WriteOrderRepository
22> 4. Update PostgresOrderRepository struct to implement both
23> 5. Run: go build ./... after each step to verify
24> 6. Do not change tests that already pass

Notice points 2 and 6: by embedding the old interface and not touching tests that are already green, this refactoring runs without breaking existing calling code.


13.4 Extract Domain Layer Refactoring

The next classic case is business logic that should live in the domain entity but is instead scattered across the usecase. The Go snippet below shows the “before” state, where business rules leak into the usecase.

go
 1// BEFORE (business logic in the usecase — WRONG):
 2func (uc *CancelOrderUseCase) Execute(ctx context.Context, input CancelOrderInput) error {
 3    order, err := uc.repo.GetByIDAndUserID(ctx, input.OrderID, input.UserID)
 4    if err != nil { return fmt.Errorf("cancelOrder.Execute: %w", err) }
 5    if order == nil { return domain.ErrOrderNotFound }
 6
 7    // Business logic in the usecase — should be in the entity!
 8    if order.Status != domain.StatusPending {
 9        return &domain.OrderNotCancellableError{Status: order.Status}
10    }
11    if time.Since(order.CreatedAt) > 30*time.Minute {
12        return domain.ErrCancelWindowExpired
13    }
14    // ...
15}

The problem is clear: the rule of “when an order may be cancelled” is domain knowledge, not usecase orchestration. The “after” version below moves that rule into methods on the Order entity.

go
 1// AFTER (business logic in the entity — CORRECT):
 2// domain/order/entity.go
 3func (o *Order) CanBeCancelled() bool {
 4    return o.Status == StatusPending
 5}
 6
 7func (o *Order) IsCancelWindowOpen() bool {
 8    return time.Since(o.CreatedAt) <= 30*time.Minute
 9}
10
11func (o *Order) ValidateCancellation() error {
12    if !o.CanBeCancelled() {
13        return &OrderNotCancellableError{Status: o.Status}
14    }
15    if !o.IsCancelWindowOpen() {
16        return ErrCancelWindowExpired
17    }
18    return nil
19}
20
21// usecase
22func (uc *CancelOrderUseCase) Execute(ctx context.Context, input CancelOrderInput) error {
23    order, err := uc.repo.GetByIDAndUserID(ctx, input.OrderID, input.UserID)
24    if err != nil { return fmt.Errorf("cancelOrder.Execute: %w", err) }
25    if order == nil { return domain.ErrOrderNotFound }
26    if err := order.ValidateCancellation(); err != nil { return err }
27    // ...
28}

Once moved, the usecase becomes lean and the business rules can be tested directly on the entity — this is the correct separation of responsibilities. The following prompt guides the AI to perform that move step by step.

bash
 1# Prompt to Claude Code:
 2claude
 3> Refactor: move the business logic from the cancel_order.go usecase
 4> into domain/order/entity.go.
 5>
 6> Business logic to move:
 7> 1. Status check → Order.CanBeCancelled() bool method
 8> 2. Time window check → Order.IsCancelWindowOpen() bool method
 9> 3. Combined validation → Order.ValidateCancellation() error method
10>
11> Steps:
12> 1. Add the methods to entity.go
13> 2. Update cancel_order.go to use the entity methods
14> 3. Update cancel_order_test.go to test the entity methods (not via the usecase)
15> 4. Run: go test -race ./... — everything must pass
16> 5. Run: go test -coverprofile=after.out ./...

The step 3 instruction is important: by testing the entity method directly, tests become more durable because they check domain behavior, not usecase implementation detail.


13.5 Naming Convention Refactoring (Large Scale)

A cross-codebase rename looks trivial but is the easiest way to cause silent damage if you’re not careful. The three-step flow below — audit first, rename with word boundaries, then verify — keeps naming standardization safe.

bash
 1# Scenario: inconsistent naming across the codebase
 2# ID fields: UserID, UserId, user_id (mixed)
 3# Need to standardize to UserID (Go convention: acronyms uppercase)
 4
 5# THE SAFE STEPS:
 6
 7# Step 1: Audit first with AI
 8claude
 9> Analyze the entire codebase and identify ALL fields/variables
10> that use inconsistent naming for UUID fields:
11> Look for: UserId, userId, user_id (as a Go struct field, not a JSON tag)
12> Expected: it should be UserID (Go convention)
13> List all files and line numbers.
14
15# Step 2: Claude generates gorename commands or direct edits
16claude
17> Based on that list, rename every occurrence of UserId → UserID
18> in all Go files (not JSON tags, not SQL, not comments).
19> Use word boundaries to avoid partial matches.
20> After each file: go build ./... to catch errors.
21
22# Step 3: Verify
23go build ./...
24go test ./...
25grep -r "UserId" --include="*.go" .  # should return empty

The key to safety is in Step 1: auditing first gives you an explicit list a human can review before the AI touches a single file.


13.6 Error Handling Standardization Refactoring

Inconsistent error handling is technical debt that spreads slowly. The audit prompt below asks the AI to categorize violations of the wrapping convention as CRITICAL or WARNING before anything is fixed.

bash
 1# Scenario: inconsistent error handling across the codebase
 2# Some places: return err (without wrapping)
 3# Some places: return errors.New("...") (without context)
 4# Need to standardize to: fmt.Errorf("pkg.Method: %w", err)
 5
 6# Audit with Claude Code:
 7claude
 8> Scan the entire internal/ directory and identify:
 9> 1. Error returns without wrapping: "return err" (without fmt.Errorf)
10> 2. Error creation without context: errors.New("message") in non-domain code
11> 3. Error wrapping without a function name: fmt.Errorf("error: %w", err)
12>    (should include the function name: fmt.Errorf("pkg.FuncName: %w", err))
13>
14> Categorize as:
15> CRITICAL: clearly violates the convention
16> WARNING: borderline case
17>
18> Output: file:line:issue for each finding
19
20# Fix in batches:
21claude
22> Fix all CRITICAL error handling issues in internal/repository/postgres/
23> (one package first).
24> After the fix: go build ./internal/repository/... && go test ./internal/repository/...

By fixing one package per batch and building+testing immediately, you keep the blast radius small so compilation errors are easy to trace back to their source.


13.7 Migrate from Package A to Package B

Migrating a dependency across the codebase needs the right order: analyze impact, update go.mod, batch update imports, verify, then drop the old dependency. The following steps migrate github.com/golang/mock to go.uber.org/mock safely.

bash
 1# Scenario: need to migrate from a deprecated package to a standard one
 2# Example: github.com/golang/mock/gomock → go.uber.org/mock/gomock
 3
 4# Step 1: Impact analysis
 5claude
 6> Identify all files that import "github.com/golang/mock/gomock"
 7> or "github.com/golang/mock/mockgen"
 8> List them all with full paths.
 9
10# Step 2: Update go.mod
11go get go.uber.org/mock/gomock@latest
12go get go.uber.org/mock/mockgen@latest
13go mod tidy
14
15# Step 3: Batch update imports
16claude
17> Update all import paths from:
18>   "github.com/golang/mock/gomock" → "go.uber.org/mock/gomock"
19>   "github.com/golang/mock/mockgen" → "go.uber.org/mock/mockgen"
20>
21> In all the files identified earlier.
22> After the update: go build ./... to verify there are no compilation errors.
23
24# Step 4: Verify
25go test -race ./...
26
27# Step 5: Remove the old dependency
28go mod edit -droprequire github.com/golang/mock
29go mod tidy

This order matters: dropping the old dependency (Step 5) is done only after the build and tests are green, so you’re never in a half-migrated state that won’t compile.


13.8 Refactoring a Monolith to Clean Architecture

Migrating a monolith to Clean Architecture is too big for a single session — it must be broken into phases that can each be merged. The four-phase plan below extracts the domain, repository, usecase, and then dependency injection in sequence.

bash
 1# Scenario: a legacy service with all logic in main.go or handlers
 2# Need to migrate to Clean Architecture
 3
 4# Phase 1: Extract Domain (2-4 days)
 5claude
 6> Analyze handlers/*.go and identify all:
 7> 1. Entities/structs representing business objects (Order, Product, User)
 8> 2. Business rules embedded in the handlers
 9> 3. Error types being used
10>
11> Propose an internal/domain/ structure that fits Clean Architecture.
12> Create the domain package skeleton WITHOUT implementation yet.
13
14# Phase 2: Extract Repository (2-3 days)
15claude
16> Move all database calls from handlers into the repository layer.
17> Create interfaces in the domain, implement in internal/repository/postgres/.
18> Handlers will call the repository directly for now (temporary).
19> Verify: go build ./... && go test ./...
20
21# Phase 3: Extract Usecase (2-3 days)
22claude
23> Move the business logic from handlers into the usecase layer.
24> The handler now only parses the request and returns the response.
25> The usecase contains all business logic and calls the repository via interfaces.
26
27# Phase 4: Dependency Injection (1 day)
28claude
29> Set up dependency injection in main.go or cmd/app/main.go.
30> Wire: DB → repository → usecase → handler → router
31> Remove all global state.

Each phase produces a state that remains buildable and testable, so you can merge between phases and never sit on one giant PR that stays risky for a week.


13.9 Refactoring with Tests as a Guard

The safest pattern for refactoring is “refactor under green tests” — every small step must stay green. The flow below formalizes that pattern, including an explicit instruction for the AI to stop the moment a test fails.

bash
 1# The safest pattern: "Refactor under green tests"
 2
 3# Before refactoring:
 4go test ./...  # must be 100% pass
 5
 6# During refactoring (each small step):
 7# 1. Make a small change
 8# 2. go test ./...  # must still pass
 9# 3. If it fails: revert the last step, analyze
10# 4. If it passes: commit and continue to the next step
11
12# The ideal AI for refactoring:
13claude
14> Refactor this step by step.
15> After EACH step, run: go build ./... && go test -race ./...
16> If there is a failure, STOP and report — do not proceed to the next step.
17> Every step must remain in a green state.

The “STOP and report” instruction turns the AI from an aggressive executor into a disciplined partner: failures are caught at the smallest step, not accumulated until the end.


13.10 Parallel Refactoring: Teams Working Together

When several developers need to refactor the same codebase, the biggest risk is not the code but merge conflicts. The strategy below uses AI to plan a refactoring order that minimizes collisions between developers.

text
 1When a team of multiple developers needs to refactor the same codebase:
 2
 3Problem:
 4  Developer A refactors the order package
 5  Developer B refactors the payment package (which depends on order)
 6  → Complex merge conflicts
 7
 8Strategy with AI:
 9  1. Pre-refactoring meeting (30 minutes):
10     "AI, we want to refactor X. What should be refactored first
11      to minimize conflicts between developers?"
12
13  2. Define clear ownership:
14     Developer A: domain layer (no external dependency)
15     Developer B: repository layer (depends on domain)
16     Developer C: usecase layer (depends on domain + repo interfaces)
17
18  3. Sequential refactoring:
19     Domain first (A) → merge → repository (B) → merge → usecase (C)
20     Not parallel! Sequential minimizes conflicts
21
22  4. Interface-first approach:
23     Define interface changes first (domain) → all developers can adapt
24     then implement changes in their respective layers

The main lesson is counter-intuitive: for cross-layer refactoring, a sequential order based on the direction of dependency is actually faster in total than parallel work that ends in conflict.


13.11 AI Refactoring Anti-Patterns

Before moving on to advanced patterns, it helps to recognize the ways AI refactoring goes wrong. The BAD vs GOOD comparison below summarizes the four most common mistakes.

text
 1Anti-pattern 1: "Refactor everything at once"
 2BAD:
 3  "Refactor the entire codebase to follow Clean Architecture"
 4
 5GOOD:
 6  "Refactor internal/usecase/order/cancel_order.go to
 7   extract business logic into the domain entity.
 8   Only this file for now."
 9
10Anti-pattern 2: "Refactor without test coverage"
11BAD:
12  Refactor code with < 50% coverage
13  No way to verify behavior is the same
14
15GOOD:
16  At least 80% coverage first, then refactor
17
18Anti-pattern 3: "Trust AI without verifying"
19BAD:
20  AI says: "Refactoring done, everything is fine"
21  Developer: pushes straight to main
22
23GOOD:
24  AI done → go build → go test -race → go vet → review diff → push
25
26Anti-pattern 4: "Too many files in one refactoring session"
27BAD:
28  "Refactor 50 files at once"
29  → If there's an error in the middle, hard to trace
30
31GOOD:
32  5-10 files per session, commit after verified

The common thread across all four anti-patterns is the same: too large a scope without a safety net. Narrow the scope and add verification, and the majority of AI refactoring problems disappear.


13.12 Refactoring Pattern: Strangler Fig for Go

Strangler Fig is a pattern for replacing legacy code gradually without a big-bang rewrite. The Go example below places the new implementation alongside the old one, then switches via a feature flag.

go
 1// Strangler Fig: replace legacy code incrementally
 2
 3// STEP 1: Create the new implementation alongside the old
 4// old_cancel_order.go (still running)
 5func CancelOrderLegacy(db *sql.DB, orderID string) error {
 6    // old implementation
 7}
 8
 9// new_cancel_order.go (new, Clean Architecture)
10type CancelOrderUseCase struct {
11    repo OrderRepository
12}
13
14func (uc *CancelOrderUseCase) Execute(ctx context.Context, input Input) error {
15    // new implementation
16}
17
18// STEP 2: Feature flag to switch
19func (h *Handler) CancelOrder(c echo.Context) error {
20    if h.featureFlags.UseNewCancelOrder {
21        return h.cancelOrderUseCase.Execute(c.Request().Context(), input)
22    }
23    return h.cancelOrderLegacy(c)
24}
25
26// STEP 3: Gradually enable the new implementation
27// Monitor: errors, latency
28// If stable for 1 week: remove the old implementation
29
30// AI is very helpful for:
31// - Generating an equivalent new implementation
32// - Verifying behavior equivalence via tests
33// - Cleaning up the old implementation after it's verified

With this pattern, you never force a total swap in a single deploy — the new implementation is raised gradually behind a feature flag and can be rolled back instantly if metrics worsen.


13.13 Refactoring Documentation for the Team

A large refactoring should leave a trail the team can read. The refactoring log template below records the motivation, changes, coverage, and rollback plan in a concise format.

markdown
 1# REFACTORING LOG — Cancel Order
 2
 3## Date
 42026-07-15
 5
 6## Type
 7Extract Domain Logic → Entity Method
 8
 9## Motivation
10Business rules (status check, time window) are scattered across multiple usecases.
11DRY violation: CancelOrder, AdminCancelOrder, BatchCancelOrder all duplicate the logic.
12
13## Changes Made
14- Added Order.ValidateCancellation() method (domain/order/entity.go)
15- Simplified cancel_order.go usecase (removed duplicated conditions)
16- Updated admin_cancel_order.go usecase (same simplification)
17- Updated batch_cancel_order.go usecase
18
19## Test Coverage
20Before: 76%
21After: 83%
22
23## Risk Assessment
24LOW: behavior preserved, tests confirm
25
26## AI Assistance
27Claude Code used for:
28- Impact analysis (which files affected)
29- Implementation of entity methods
30- Test generation for entity methods
31- Verification that all tests still pass
32
33## Rollback Plan
34git revert [commit hash] — single commit, clean rollback

A document this simple becomes a valuable audit trail when, months later, you need to answer “why was this code refactored this way?”


13.14 Tips & Gotchas

Tip 1: Refactor during quiet working hours

Complex refactoring needs high focus. Avoid packed meeting hours. Set aside a 2-4 hour time block.

Tip 2: Commit every verified step

Every small step that has passed go test = a commit. This lets you bisect if an issue appears later.

Tip 3: Small refactoring PRs

One PR = one kind of refactoring (e.g., only extract interface). Don’t combine refactoring with a feature in one PR. Reviewers can stay focused.

Tip 4: Announce refactoring to the team

Before pushing a PR that affects many files: “I’ll merge refactoring X tomorrow morning. Who is currently working on the affected files?”

Gotcha 1: An AI that’s too aggressive

Claude Code or Cursor sometimes refactor more than asked. Always check git diff before committing — read every change.

Gotcha 2: Tests that test implementation (not behavior)

Correct refactoring does not change behavior. If tests break even though the logic is the same, those tests probably test implementation detail. Fix the tests, don’t stop the refactoring.

Gotcha 3: Incomplete rename

The AI renames in Go files but misses: SQL queries, config files, API documentation, error messages. Manually check after the AI renames.


13.15 Complete Refactoring Checklist

So that no safe step is skipped, use the complete checklist below, which divides refactoring into three phases: before, during, and after, including what the reviewer must check.

text
 1Pre-refactoring:
 2□ Feature branch created
 3□ All tests pass
 4□ Coverage baseline recorded
 5□ Team notified (if it affects many files)
 6□ Refactoring scope clearly defined
 7
 8During refactoring:
 9□ Step by step, not all at once
10□ go build after each step
11□ go test after each step
12□ git commit after each verified step
13
14Post-refactoring:
15□ go test -race ./... (full test suite)
16□ go vet ./...
17□ golangci-lint run ./...
18□ Coverage does not drop from the baseline
19□ git diff review (no unintended changes)
20□ PR with a clear description (what and why)
21
22Code review for a refactoring PR:
23□ Same behavior (tests prove it)
24□ No accidental feature changes
25□ A clear architecture improvement
26□ CLAUDE.md updated if there's a new convention

This checklist turns “a feeling of safety” into objective criteria — a refactoring is considered done only when every box is checked, not when the code “looks right”.


13.16 Case Study: Refactoring Flash Sale Duplicate Logic

Let’s look at a real case: flash sale logic duplicated in three places. The “before” Go snippet below shows time and quota checks copy-pasted between usecases.

go
 1// BEFORE: Logic duplicated in 3 places
 2
 3// claim_voucher_usecase.go
 4func (uc *ClaimVoucherUseCase) Execute(ctx context.Context, input Input) error {
 5    flashSale, _ := uc.repo.GetActive(ctx, input.FlashSaleID)
 6    if flashSale == nil { return ErrFlashSaleNotFound }
 7    // Duplicate: time check
 8    if time.Now().Before(flashSale.StartTime) { return ErrFlashSaleNotStarted }
 9    if time.Now().After(flashSale.EndTime) { return ErrFlashSaleEnded }
10    // Duplicate: claim check
11    if flashSale.CurrentClaims >= flashSale.MaxClaims { return ErrVoucherSoldOut }
12    // ...
13}
14
15// admin_extend_flashsale_usecase.go
16func (uc *AdminExtendFlashSaleUseCase) Execute(ctx context.Context, input Input) error {
17    flashSale, _ := uc.repo.GetByID(ctx, input.FlashSaleID)
18    if flashSale == nil { return ErrFlashSaleNotFound }
19    // Duplicate: same time check logic!
20    if time.Now().Before(flashSale.StartTime) { return ErrNotStartedYet }
21    // ...
22}

This duplication is a time bomb: the moment the time rule changes, you have to remember every place that copied it. The prompt below asks the AI to extract that logic into entity methods.

bash
 1# Prompt to refactor:
 2claude
 3> Identify all business logic in the flashsale domain that is duplicated
 4> across multiple usecases (claim_voucher, admin_extend, batch_expire).
 5>
 6> Extract into entity methods in internal/domain/flashsale/entity.go:
 7>   FlashSale.IsActive() bool
 8>   FlashSale.CanClaimVoucher() error  (return a specific error)
 9>   FlashSale.IsClaimable() bool
10>
11> Update all usecases to use the entity methods.
12> Run tests after each update.
13> Output: coverage before and after.

The “after” result below moves all the rules into the entity, so each usecase only calls a single method and no longer keeps a copy of the logic.

go
 1// AFTER: Logic in the entity
 2
 3// internal/domain/flashsale/entity.go
 4type FlashSale struct {
 5    ID            uuid.UUID
 6    StartTime     time.Time
 7    EndTime       time.Time
 8    MaxClaims     int
 9    CurrentClaims int
10}
11
12func (fs *FlashSale) IsActive() bool {
13    now := time.Now()
14    return !now.Before(fs.StartTime) && !now.After(fs.EndTime)
15}
16
17func (fs *FlashSale) CanClaimVoucher() error {
18    if time.Now().Before(fs.StartTime) {
19        return ErrFlashSaleNotStarted
20    }
21    if time.Now().After(fs.EndTime) {
22        return ErrFlashSaleEnded
23    }
24    if fs.CurrentClaims >= fs.MaxClaims {
25        return ErrVoucherSoldOut
26    }
27    return nil
28}
29
30// Usecases are now lean:
31func (uc *ClaimVoucherUseCase) Execute(ctx context.Context, input Input) error {
32    flashSale, err := uc.repo.GetActive(ctx, input.FlashSaleID)
33    if err != nil { return fmt.Errorf("claimVoucher.Execute: %w", err) }
34    if flashSale == nil { return ErrFlashSaleNotFound }
35    if err := flashSale.CanClaimVoucher(); err != nil { return err }
36    // ...
37}

A single source of truth for the flash sale rules means a rule change is made in just one method — precisely the main selling point of extracting domain logic.


13.17 Automated Refactoring via CI

Some refactoring signals can be detected automatically in CI before a human reviewer gets involved. The GitHub Actions workflow below checks for logic duplication, interface size, and architecture violations on every pull request.

yaml
 1# Detect potential refactoring opportunities in CI
 2# .github/workflows/refactoring-lint.yml
 3
 4name: Refactoring Opportunities
 5
 6on: [pull_request]
 7
 8jobs:
 9  refactoring-check:
10    runs-on: ubuntu-latest
11    steps:
12      - uses: actions/checkout@v4
13      - uses: actions/setup-go@v5
14        with: { go-version: '1.22' }
15
16      - name: Check for duplicate logic
17        run: |
18          # Simplified: check for functions that are identical or very similar
19          # Tools like jscpd or duplo can be used
20          go install github.com/mibk/dupl@latest
21          dupl -t 15 ./internal/usecase/...
22
23      - name: Check interface size
24        run: |
25          # Warn if an interface has > 5 methods
26          go run scripts/check-interface-size.go ./internal/domain/...
27
28      - name: Check architecture violations
29        run: |
30          go run scripts/check-layer-imports.go ./...

With CI like this, “refactoring debt” becomes visible the moment a PR is opened, rather than only being discovered after it has piled up for months.


13.18 Tools for Refactoring Support

Besides AI, there are native Go tools that greatly help refactoring with semantic awareness. The set of commands below shows how to use guru, gorename, gopls, staticcheck, and go mod graph, then orchestrate them through Claude Code.

bash
 1# Tool 1: guru (Go analysis tool)
 2go get golang.org/x/tools/cmd/guru
 3# Example: find all callers of a method
 4guru -scope ./... callers internal/domain/order/entity.go:#245
 5
 6# Tool 2: gorename
 7go get golang.org/x/tools/cmd/gorename
 8# Rename with semantic awareness
 9gorename -from '"github.com/santekno/santekno-shop/internal/domain/order".OrderRepository.GetByID' \
10         -to GetByIDAndUserID
11
12# Tool 3: gopls rename (via IDE)
13# VS Code: F2 on a symbol → Go-aware rename
14
15# Tool 4: staticcheck to detect dead code
16go install honnef.co/go/tools/cmd/staticcheck@latest
17staticcheck -checks U1000 ./...
18# U1000: exported func/var/type never used
19
20# Tool 5: go mod graph for dependency analysis
21go mod graph | grep santekno/shop
22
23# All of these tools can be orchestrated via Claude Code:
24claude
25> Run staticcheck to detect dead code in internal/usecase/
26> Identify what can be removed as part of the refactoring

The best combination is AI for reasoning and planning, plus semantic tools like gorename/gopls for renames that are truly type- and scope-aware.


13.19 Commit Strategy for Large Refactorings

Large refactorings need a clear commit strategy so the git history stays readable and bisectable. The three strategies below — atomic commits, squash for PRs, and separating concerns — cover the majority of needs.

bash
 1# Large refactorings need a clear commit strategy
 2
 3# Strategy 1: Atomic commits (recommended)
 4# One commit = one logical, verified change
 5
 6git commit -m "refactor(order): extract CanBeCancelled to entity method"
 7git commit -m "refactor(order): extract IsCancelWindowOpen to entity method"
 8git commit -m "refactor(order): extract ValidateCancellation combining both checks"
 9git commit -m "refactor(usecase): use entity.ValidateCancellation in cancel_order"
10git commit -m "refactor(usecase): use entity.ValidateCancellation in admin_cancel"
11git commit -m "refactor(test): update tests to test entity methods directly"
12
13# Strategy 2: Squash for the PR (cleaner history)
14git rebase -i HEAD~6
15# squash everything into one commit:
16# "refactor(order): extract business rules to domain entity"
17
18# Strategy 3: Separate commits for separate concerns
19# NEVER mix: refactoring + feature + bug fix in one commit

The golden rule that must not be broken is in Strategy 3: never mix refactoring, feature, and bug fix in one commit, because it destroys the ability to bisect and review.


13.20 Final Verification Before Merge

Before merging a refactoring PR, run a thorough final verification in one breath. The script below chains build, test with the race detector, vet, lint, and a coverage comparison.

bash
 1# Final verification checklist before merging a refactoring PR:
 2
 3echo "=== BUILD ===" && go build ./... && echo "Build OK"
 4echo "=== TESTS ===" && go test -race -count=1 ./... && echo "Tests OK"
 5echo "=== VET ===" && go vet ./... && echo "Vet OK"
 6echo "=== LINT ===" && golangci-lint run ./... && echo "Lint OK"
 7echo "=== COVERAGE ===" && \
 8  go test -coverprofile=after.out ./... && \
 9  go tool cover -func=after.out | tail -1 && \
10  echo "(compare with the pre-refactoring baseline)"
11
12# Everything must be green before the PR is merged

Running everything in one script makes the merge gate binary: all green means safe, one red means not yet worthy of merge — there is no gray area.


13.21 Combining Refactoring with a Feature Sprint

A practical question that often comes up: how do you balance refactoring with shipping features? The three options below — a dedicated sprint, the 20% rule, and refactoring as part of a feature — each have their own trade-offs.

text
 1Best practice for balancing refactoring with feature delivery:
 2
 3Option A: Dedicated Refactoring Sprint (every 3-4 sprints)
 4  Sprint 1-3: Feature delivery
 5  Sprint 4: Dedicated tech debt / refactoring
 6
 7  Pro: Focus, no distraction
 8  Con: The team may resist "not shipping features"
 9
10Option B: 20% Rule
11  80% of sprint capacity for features
12  20% for refactoring and tech debt
13
14  Pro: Sustainable, continuous improvement
15  Con: Needs discipline to enforce the percentage
16
17Option C: Refactoring as Part of a Feature
18  When implementing a feature that touches file X:
19  "Refactor file X a bit before adding the new feature"
20
21  Pro: Boy Scout Rule ("leave code better than you found it")
22  Con: Scope creep risk, needs a strict boundary
23
24Recommendation for the Santekno team with AI tools:
25  Option B (20% rule) with AI acceleration.
26  20% of a sprint = 4 days → with AI, you can accomplish 40 days' worth of refactoring.

The recommendation is clear: a sustainable 20% rule, multiplied by AI acceleration, gives continuous improvement without sacrificing feature delivery.


13.22 Summary

Refactoring is an important investment in long-term code health, and with AI this investment becomes far more affordable. To separate what changes from what stays the same, let’s summarize with these two lists.

What changes with AI:

  • Impact analysis: from 2 hours manual → 5 minutes with AI
  • Bulk changes: from 1 day → 30-60 minutes
  • Documentation: auto-generated refactoring log

What does NOT change:

  • The need for comprehensive tests
  • Human judgment for scope and approach
  • Code review to verify correctness
  • Communication to the team

The key point: AI accelerates execution, but humans remain responsible for decisions on scope, review, verification, and communication. The most valuable refactoring — extracting business logic into a domain entity — is the one most often deferred and yet the most impactful for long-term maintainability.

Related Articles

💬 Comments