The Refactoring Risk Assessor: Turn Messy Code Into a Prioritized Cleanup Plan

Why this prompt matters
Refactoring proposals get rejected or turn into open-ended rewrites for the same reason: without a risk/effort breakdown, “we should clean this up” sounds like unbounded work with no clear payoff, so teams either approve a full rewrite that stalls for months or reject cleanup entirely and let the next incident happen in the same code.
What we use it for
A senior engineer inherits a payment webhook handler that has caused three production incidents in the last quarter and needs to convince a skeptical team lead to allocate cleanup time instead of just adding another patch.
Prompt
Act as a senior software engineer conducting a pre-refactor risk assessment — the kind you'd do before asking a team lead to allocate sprint time to cleanup work, not a casual code review. Context: Language/framework: [LANGUAGE OR FRAMEWORK, e.g., "Python/Django"]. Here is the code with known problems: [PASTE YOUR CODE HERE]. Why I want to refactor it: [REASON, e.g., "every feature added to this file causes a regression elsewhere" or "onboarding a new engineer here takes two weeks instead of two days"]. Hard constraints I'm working under: [CONSTRAINTS, e.g., "cannot change the public API," "must ship in small increments," "test coverage on this file is under 20%"]. Task: Identify every distinct problem in the code (not just the obvious one). For each problem, assess: (a) the risk of leaving it as-is, specifically what breaks and under what conditions, (b) the effort to fix it, in rough size terms (small/medium/large), and (c) whether it's safe to fix without new tests first. Then produce concrete before/after code examples for the two or three highest-priority problems. Finally, group everything into a phased plan ordered by risk-reduction-per-unit-effort, not by what's most satisfying to fix first. Constraints: - Do not recommend a full rewrite unless you can name the specific problem that makes incremental fixes impossible. Default assumption is that incremental fixes are possible. - Every phase must be independently shippable — no phase should depend on a later phase being finished first. - If a fix requires test coverage that doesn't exist yet, say so explicitly and put "write tests for X" as its own line item before the fix, not bundled into it. - Do not flag stylistic preferences (naming, formatting) as risk items unless they're actively causing the stated problem. Output Format: 1. A table: Problem | Risk if left unaddressed | Fix effort (S/M/L) | Needs new tests first? (Y/N) 2. Before/after code blocks for the top 2-3 priority items, each with a one-line explanation of what changed and why it's safe 3. A phased plan: Phase 1 (ship this week), Phase 2 (ship this sprint), Phase 3 (needs test coverage or design discussion first)
Result
Context provided: A 40-line Python function in a Django app that validates and processes incoming webhook payloads from a payment provider. Reason for refactor: "every time we add a new payment event type, someone breaks the refund flow." Constraint: "can't change the public webhook URL or the response format the payment provider expects."
1. Risk/Effort Table
| Problem | Risk if left unaddressed | Fix effort | Needs tests first? |
|---|---|---|---|
| Event type dispatch is a 15-branch if/elif chain | New event types get inserted in the wrong branch order; refund logic silently skipped for event types added after it | M | Y |
| Signature verification and business logic are in the same function | Cannot unit test business logic without mocking HTTP signature headers | S | N |
| Refund amount is recalculated in three places with slightly different rounding | Silent off-by-cents discrepancies on high-volume days | S | Y |
| No idempotency check on webhook retries | Duplicate refunds if the payment provider retries a slow response | M | Y |
2. Before/After — Priority 1 (dispatch chain)
Before: if event_type == "charge.success": ... elif event_type == "refund.issued": ... elif event_type == "charge.failed": ... (15 branches, refund logic on branch 11)
After: HANDLERS = {"charge.success": handle_charge, "refund.issued": handle_refund, ...}; HANDLERS.get(event_type, handle_unknown)(payload) — safe because new event types now require an explicit registry entry instead of falling through an unrelated elif chain, and missing handlers fail loudly instead of silently matching the wrong branch.
3. Phased Plan
Phase 1 (this week): Extract signature verification into its own function (S, no tests needed — pure extraction, behavior unchanged). Write unit tests for the current dispatch behavior before touching it.
Phase 2 (this sprint): Replace the if/elif chain with the handler registry, using the Phase 1 tests as a safety net. Consolidate the three refund rounding implementations into one function.
Phase 3 (needs design discussion): Add idempotency keys — this touches the database schema (a new "processed_webhook_ids" table) and needs a decision on retention period, so it's not a pure refactor.
Refactoring proposals fail for a predictable reason: they arrive as a feeling ("this code is bad") instead of a plan (what's risky, what's cheap to fix, what order to fix it in). A team lead facing a vague cleanup request has two options — approve an open-ended rewrite that has no clear finish line, or reject it and let the underlying problem cause the next incident. Neither is good, and both come from the same root cause: nobody separated "this is unpleasant to read" from "this will break in a specific, predictable way."
Why this prompt separates risk from effort
The prompt forces two independent judgments per problem — how bad is it if we do nothing, and how expensive is it to fix — instead of one combined "priority" score. This matters because the two don't correlate. Some of the scariest code (a 15-branch dispatch chain hiding a silent failure mode) is cheap to fix. Some of the ugliest code (deeply nested legacy logic nobody wants to touch) might carry low actual risk if it's stable and rarely modified. Collapsing both into a single priority number hides exactly the information a team lead needs to make a sequencing decision.
Why every phase has to be independently shippable
The constraint that no phase can depend on a later phase finishing is there because refactoring efforts die in the gap between "started" and "finished." A three-phase plan where phase 2 requires phase 3's database migration to be safe is really one big undertaking wearing a phased costume — if the team gets pulled onto something else after phase 1, you're left with a half-refactored system, which is often worse than the original mess. Forcing independent shippability means partial completion still leaves the codebase in a strictly better state than before.
Why the prompt won't let it recommend a rewrite by default
Full rewrites are the refactoring equivalent of a doctor recommending amputation for a sprained ankle — sometimes correct, usually not, and it's the option engineers reach for when they haven't done the harder work of identifying which specific parts are actually broken. Requiring the model to name the specific blocker that makes incremental fixes impossible (not just "the code is old" or "the code is messy") keeps the default answer honest: most code can be improved in place.
Why "needs tests first" is its own column, not a footnote
The most common way refactoring goes wrong isn't bad judgment about what to fix — it's fixing something correctly-identified as risky without a safety net to catch a regression. Making "needs tests first" an explicit yes/no column, and requiring it as a separate line item rather than bundled into the fix, stops the common failure mode where a team decides testing can happen "at the same time" as the refactor and then skips it under time pressure.
How to adapt this for your own codebase
The prompt works on a single function or a whole module — for larger scopes, expect the risk/effort table to run longer and the before/after examples to focus on just the top 2-3 items, which is intentional; a refactoring plan with ten simultaneous before/after examples is not a plan a team can actually execute in order.