Refactor Legacy Code
Every long-running outsourcing project accumulates debt. Controllers that grew to 800 lines, business logic buried in view templates, duplicated validation spread across 12 files. The client knows it is a problem — they feel it every time a “simple change” takes a week. But refactoring without tests, without a plan, and with a team that rotated six months ago is how you introduce regressions that cost you the relationship. Sun Agent Kit scouts the debt, builds an incremental refactoring plan with a test safety net, and executes each extraction in a sequence that keeps the application running at every step.
Overview
Goal: Systematically extract and restructure accumulated tech debt in a long-running project without breaking existing functionality
Time: 1–3 hours for a focused module (vs 1–3 days of risky manual refactoring)
Agents used: scout, planner, implementer, tester, reviewer
Commands: /sk:scout, /sk:plan, /sk:cook, /sk:test, /sk:code-review
Prerequisites
- Sun Agent Kit installed and authenticated (installation guide)
- Git repository with a clean working tree (commit or stash everything before starting)
- At least a partial test suite — even low coverage is better than none
- Clarity on which module or feature area is being targeted (do not attempt “refactor everything”)
Step-by-Step Workflow
Step 1: Scout for tech debt and build a debt map
Before touching a single line, get a full picture of what is broken and why.
/sk:scout "large classes, long methods, duplicated logic, missing tests, god objects, business logic in controllers, raw SQL in views"
What happens: The agent:
- Scans the codebase for structural debt indicators — oversized files, long methods, duplicated code blocks
- Identifies god objects (classes handling multiple unrelated concerns)
- Detects dependency tangles and circular references
- Generates a debt map report saved to
plans/reports/
Step 2: Plan the refactoring sequence
The agent creates an ordered plan that respects dependencies — you cannot extract an interface that depends on a class you have not yet isolated.
/sk:plan "Refactor OrderService.js — extract pricing, inventory, and notification concerns into separate services. Preserve all existing behavior."
What happens: The agent:
- Analyzes the target file’s dependency graph — methods, external dependencies, and call sites
- Identifies which concerns can be extracted independently
- Orders extraction steps so each one is safe (no circular dependencies mid-refactor)
- Saves the plan with estimated steps to
plans/
Step 3: Add characterisation tests before touching production code
This is the safety net. Characterisation tests lock in the current behavior — they do not test what the code should do, they test what it does do. Regressions become impossible to miss.
/sk:cook "Add characterisation tests for OrderService.js — cover all public methods, capture current return values and side effects including edge cases"
What happens: The agent:
- Analyzes all public methods and catalogs side effects (DB writes, emails, logs)
- Generates a test suite that captures current behavior with spy setup for side effects
- Runs the generated tests against the unmodified code to confirm the baseline passes
Step 4: Extract the first service (PricingService)
Extract one concern at a time. Each extraction is small enough to review and revert if needed.
/sk:cook "Extract PricingService from OrderService.js — move calculateTotal(), applyDiscount(), calculateTax(), formatCurrency() into src/services/PricingService.js"
What happens: The agent:
- Moves the specified methods into a new service file
- Updates the original file to delegate to the new service via dependency injection
- Runs characterisation tests to confirm no behavior changes
- Runs the full test suite to verify no regressions
Step 5: Repeat extraction for remaining concerns
Run the same pattern for InventoryService and NotificationService. Each step takes minutes and the test suite validates every one.
/sk:cook "Extract InventoryService from OrderService.js — move checkStock(), reserveStock(), releaseStock() into src/services/InventoryService.js"
/sk:cook "Extract NotificationService from OrderService.js — move sendOrderConfirmation(), sendShippingUpdate(), sendCancellationEmail() into src/services/NotificationService.js"
What happens: For each extraction, the agent moves methods, updates delegation, and runs both characterisation tests and the full test suite to confirm zero regressions.
Step 6: Review the refactored code and commit
Once all extractions are done, run a code review to confirm the refactored structure matches the plan.
/sk:code-review --pending
What happens: The agent:
- Reviews the structural changes — file size reduction, new service files, concern separation
- Checks that dependency injection is consistent across all extracted services
- Confirms no new code smells were introduced
- Produces a summary with the debt reduction achieved
Complete Example: 2-Year-Old Monolith Needs Module Extraction
Scenario
A Vietnamese fintech client has a two-year-old Laravel monolith that started as a simple loan management tool and became, through successive feature additions by four different development teams, a large codebase where the LoanController handles loan application, credit scoring, repayment scheduling, SMS notifications, PDF generation, and accounting journal entries. The current team (rotated in six months ago) is afraid to touch it — every change breaks something unexpected. The client wants a new mobile API, but the controller cannot be safely extended in its current state.
Commands chained
# Week 1 Day 1 — understand the full scope of debt
/sk:scout "large classes, duplicated logic, missing tests, god objects, business logic in controllers, circular dependencies"
# Generate a prioritised refactoring plan
/sk:plan "Create phased refactoring plan for LoanController.php — extract CreditScoringService, RepaymentService, NotificationService, PDFService, AccountingService"
# Day 2 — safety net first
/sk:cook "Add characterisation tests for LoanController — cover all public methods, capture all side effects"
# Day 3 — start with the least-entangled service
/sk:cook "Extract CreditScoringService from LoanController — move all credit evaluation logic"
# Day 4 — next extraction
/sk:cook "Extract RepaymentService from LoanController — move schedule generation, payment processing, late fee calculation"
# Week 2 — continue extractions
/sk:cook "Extract NotificationService from LoanController — SMS and email handling, templating"
/sk:cook "Extract PDFService from LoanController — loan agreement and statement generation"
/sk:cook "Extract AccountingService from LoanController — journal entry creation, GL posting"
# Final — review the completed refactoring
/sk:code-review --pending
# Document the new architecture for the team
/sk:docs "Generate architecture documentation for the new service layer — class diagram, dependency graph, how to add a new loan type"
Result
Over two weeks, the LoanController shrinks from a massive god object to a thin orchestration layer. Five dedicated services are independently testable. The new mobile API endpoint is added in hours by a junior engineer who had never touched the code before — using the extracted services directly. The client’s comment: “It feels like a different codebase.”
Time Comparison
| Task | Manual | With Sun Agent Kit |
|---|---|---|
| Mapping tech debt across codebase | 4 hours | minutes |
| Planning extraction sequence (safely) | 2 hours | minutes |
| Writing characterisation tests | 3 hours | minutes |
| Each service extraction + verification | 60 min each | minutes each |
| Code review of refactored result | 60 min | minutes |
| Updating team documentation | 90 min | minutes |
| Total (5-service extraction) | ~16 hours | ~2 hours |
Best Practices
1. Write characterisation tests before any extraction ✅
The characterisation test suite is the only reliable way to know you have not broken anything. Do not start an extraction without one. A test suite that takes minutes to generate is infinitely better than discovering a regression in production three weeks later.
2. Extract one concern per PR ✅
Small, focused PRs are reviewable. A PR that extracts PricingService and InventoryService and refactors the controller and adds tests is not reviewable — it is a merge-and-hope. Each extraction is a logical unit: one service, one PR, one review, one merge. The plan the agent generates is sequenced exactly this way.
3. Do not refactor and add new features in the same branch ❌
The temptation is to “clean this up while I am in here.” This conflates two independent changes and makes regressions impossible to attribute. If a bug is found in a refactoring + feature branch, you do not know which caused it. Keep refactoring branches pure — no new behavior, only restructuring.
4. Do not skip the scout step because you “already know the debt” ❌
Every developer who has worked in a codebase for six months thinks they know all the problems. The scout reliably surfaces debt that familiarity hides — duplicate logic spread across files written by different engineers, or a circular dependency that explains why a “simple” change requires touching many files. Read the debt map before planning.
Troubleshooting
Problem: Characterisation tests fail on the unmodified code
Solution: This means the current tests have environment dependencies — database state, external API calls, or time-sensitive logic — that the characterisation generator did not account for. Run /sk:cook "Fix characterisation test environment setup — add database seeding and mock external calls". The goal is a deterministic test suite that passes on any machine.
Problem: After extraction, integration tests fail even though unit tests pass
Solution: Extraction sometimes reveals that two services shared state through a module-level variable or a singleton that was not visible at the unit level. Run /sk:debug "integration test failures after service extraction" and describe the failure — the agent will locate the shared state and suggest whether to inject it explicitly or move it to a configuration layer.
Problem: The extracted service has too many constructor parameters (constructor bloat)
Solution: Seven or more constructor parameters on a service is a sign that the service itself needs to be split further. Run /sk:plan "split [ServiceName] — identify sub-concerns" to get a second-level extraction plan. The agent will identify which groups of methods within the service naturally cluster together.
Problem: The client’s CI pipeline fails after refactoring merges due to import path changes
Solution: Run /sk:cook "Update all import paths after service extraction — find and replace old module references" with the list of moved files. The agent generates updates for every import statement in the codebase.
Next Steps
- Code Review — Gate every extraction PR through a code review before merging
- Optimize Performance — After refactoring, profile the restructured code to confirm no unintended performance impact
- Generate Documentation — Document the new architecture so the next team rotation starts with a map, not a mystery
Key takeaway: Legacy code is not refactored by rewriting everything at once — it is refactored incrementally, one extracted service at a time, with a test safety net ensuring nothing breaks along the way.