Legacy refactoring isn't the same as rewriting code. Rewriting is easy to describe; refactoring is harder because the old behavior still matters. A legacy system may look messy, but it usually contains years of product decisions, bug fixes, customer exceptions, reporting dependencies, and emergency patches. Some of those decisions are documented. Many are not.

That's why refactoring legacy code is risky. You're not just asking, "Can I make this code cleaner?" You're asking, "Can I make this code cleaner without changing behavior that someone depends on?"

Claude Code can help a lot here, but only if you use it in the right order. The safe order is understand first, test second, refactor third. Not the other way around.

The Old Way: Open File, Feel Pain, Start Editing

Many legacy refactors start with frustration. You open a large controller, you see 800 lines, and you see SQL, validation, business rules, logs, API calls, and email sending packed into one method. You think, "This needs to be cleaned up." And you're right.

But if you start moving code immediately, you may break behavior you didn't even know existed. Here's a simplified example:

PHP
public function cancel(Request $request, int $subscriptionId)
{
    $subscription = Subscription::findOrFail($subscriptionId);

    if ($subscription->status === 'past_due') {
        return response()->json([
            'message' => 'Please update payment method before cancellation.',
        ], 422);
    }

    if ($subscription->plan_code === 'legacy_lifetime') {
        Log::warning('Attempt to cancel legacy lifetime subscription', [
            'subscription_id' => $subscription->id,
        ]);

        return response()->json([
            'message' => 'Contact support to manage this plan.',
        ], 422);
    }

    $subscription->status = 'cancelled';
    $subscription->cancelled_at = now();
    $subscription->save();

    CancelSubscriptionJob::dispatch($subscription->id);

    return response()->json(['status' => 'ok']);
}

This controller isn't beautiful, but it contains behavior:

  • past-due subscriptions can't be cancelled
  • legacy lifetime plans are handled manually
  • cancellation updates two fields
  • a job is dispatched after save
  • response status codes matter
  • error messages may be used by frontend clients

A careless refactor can easily change one of these. Claude can help you slow down.

Step One: Ask Claude To Read Before It Writes

Your first prompt shouldn't be "refactor this." Use this instead:

Text
Analyze this controller action as legacy production code.
Do not refactor yet.
List all observable behaviors, side effects, response codes, database writes, dispatched jobs, and business rules.
Also list what tests should exist before refactoring.

This kind of prompt forces the model to produce a behavior map. For the example above, the behavior map may include:

Text
Observable behaviors:
- returns 422 for past_due subscriptions
- returns 422 for legacy_lifetime plans
- returns 200 with status ok after successful cancellation

Side effects:
- writes status = cancelled
- writes cancelled_at = current timestamp
- dispatches CancelSubscriptionJob
- logs warning for legacy lifetime cancellation attempts

Risk during refactoring:
- changing response body may break frontend clients
- moving dispatch before save may change job behavior
- removing log may break operations visibility

That's the refactoring checklist. Before you touch the code, you already know what must stay true.

Refactoring safety map: a central Old Controller block connected to five labeled outputs (HTTP Responses, DB Writes, Queue Jobs, Logs & Events, Frontend Behavior), each paired with a red Do Not Break contract panel listing the specific behaviors that must remain unchanged during a refactor.

Step Two: Create Characterization Tests

Characterization tests describe how the system works today. They don't prove the behavior is correct or perfect; they protect the current behavior while you refactor. In legacy systems that distinction matters a lot. A characterization test essentially says: "Before I improve this code, I want proof that the important existing behavior still works after my changes."

Claude can help generate a first draft:

Text
Write characterization tests for this cancellation action.
Use PHPUnit and Laravel feature tests.
Protect response codes, database changes, and job dispatching.
Do not assume behavior that is not visible in the code.

Possible tests:

PHP
public function test_past_due_subscription_cannot_be_cancelled(): void
{
    $subscription = Subscription::factory()->create([
        'status' => 'past_due',
    ]);

    $response = $this->postJson("/subscriptions/{$subscription->id}/cancel");

    $response->assertStatus(422)
        ->assertJson([
            'message' => 'Please update payment method before cancellation.',
        ]);

    $this->assertDatabaseHas('subscriptions', [
        'id' => $subscription->id,
        'status' => 'past_due',
    ]);
}
PHP
public function test_successful_cancellation_updates_subscription_and_dispatches_job(): void
{
    Queue::fake();

    $subscription = Subscription::factory()->create([
        'status' => 'active',
        'plan_code' => 'standard_monthly',
        'cancelled_at' => null,
    ]);

    $response = $this->postJson("/subscriptions/{$subscription->id}/cancel");

    $response->assertOk()
        ->assertJson(['status' => 'ok']);

    $this->assertDatabaseHas('subscriptions', [
        'id' => $subscription->id,
        'status' => 'cancelled',
    ]);

    Queue::assertPushed(CancelSubscriptionJob::class);
}

These tests aren't fancy, and that's the point. They create a safety net. Once they pass, you can refactor with more confidence.

Step Three: Ask Claude To Identify Dependencies

Legacy code often depends on things you don't see in one file. A controller may be called by:

  • web frontend
  • mobile app
  • admin panel
  • scheduled script
  • webhook retry
  • internal integration

Before refactoring, ask Claude:

Text
Find all references to this cancellation endpoint, controller method, job, and subscription status values.
List possible callers and downstream dependencies.
Do not change code.

You want to discover surprises. Maybe cancelled_at is used in revenue reports. Maybe CancelSubscriptionJob sends data to a billing provider. Maybe the frontend checks the exact error message, or mobile clients still expect a 200 response in some old flow.

Claude can help search and summarize, but you should verify the important parts manually, especially if the result affects production behavior.

Step Four: Refactor In Small Steps

After you understand the behavior and add tests, you can refactor. Small steps are safer than one giant rewrite.

Bad prompt:

Text
Rewrite this controller using clean architecture.

Better prompt:

Text
Refactor this controller in the smallest safe step.
Keep public behavior exactly the same.
Use the existing tests as safety checks.
Only extract cancellation decision logic into a private method first.
Do not rename routes, change responses, or change job dispatch order.

A first step might look like this:

PHP
public function cancel(Request $request, int $subscriptionId)
{
    $subscription = Subscription::findOrFail($subscriptionId);

    if ($response = $this->blockedCancellationResponse($subscription)) {
        return $response;
    }

    $subscription->status = 'cancelled';
    $subscription->cancelled_at = now();
    $subscription->save();

    CancelSubscriptionJob::dispatch($subscription->id);

    return response()->json(['status' => 'ok']);
}

private function blockedCancellationResponse(Subscription $subscription): ?JsonResponse
{
    if ($subscription->status === 'past_due') {
        return response()->json([
            'message' => 'Please update payment method before cancellation.',
        ], 422);
    }

    if ($subscription->plan_code === 'legacy_lifetime') {
        Log::warning('Attempt to cancel legacy lifetime subscription', [
            'subscription_id' => $subscription->id,
        ]);

        return response()->json([
            'message' => 'Contact support to manage this plan.',
        ], 422);
    }

    return null;
}

This isn't the final architecture, but it's safer. You extracted one concept without changing the external behavior. Run tests, commit, then continue.

Legacy refactoring staircase diagram: seven numbered steps rising left to right above a labeled production-risk gap. Steps in order: Behavior Map, Characterization Tests, Dependency Scan, Small Extraction, Test Run, Commit, Repeat. Calm blue and green tones on a light background.

Step Five: Compare Behavior Before And After

One of the best uses of Claude is comparing two versions of code. After a refactor, ask:

Text
Compare the old version and new version of this controller.
Focus only on behavior changes.
Check response codes, response body, database writes, job dispatch order, logs, exceptions, and edge cases.
If behavior changed, show exactly where.

This isn't a replacement for tests. It's an extra review layer. Claude may catch things like:

Text
Potential behavior change:
The old code logged warning before returning for legacy_lifetime plans.
The new code returns before logging.

Or:

Text
Potential behavior change:
The old code dispatched CancelSubscriptionJob after saving the subscription.
The new code dispatches before saving.

These are exactly the small mistakes that happen during cleanup, and exactly the kind that are easy to miss in a large diff.

Use Claude To Create A Refactoring Journal

A refactoring journal is a short document that records what you learned, and it's useful for the team. Prompt:

Text
Create a refactoring journal entry for this change.
Include:
- original problems
- behavior protected by tests
- dependencies discovered
- what changed structurally
- what did not change behaviorally
- remaining cleanup ideas

Example:

Markdown
## Refactoring Journal: Subscription Cancellation

### Original problem

The cancellation controller mixed HTTP response handling, business decisions, logging, database writes, and job dispatching.

### Protected behavior

- past-due subscriptions return 422
- legacy lifetime plans return 422 and write warning log
- successful cancellation sets status and cancelled_at
- cancellation job is dispatched after save

### Structural change

Cancellation blocking logic was extracted into a dedicated method.

### Behavior intentionally unchanged

Response payloads, status codes, and job dispatch order remain unchanged.

This kind of documentation is boring today and priceless six months later.

What Claude Changes About Legacy Refactoring

Claude doesn't remove the need for engineering discipline. It makes discipline easier to apply. It helps you:

  • read more code faster
  • find related files
  • summarize side effects
  • generate characterization test drafts
  • compare behavior before and after
  • document what changed
  • spot risky assumptions

But it can't guarantee correctness. It can miss dynamic behavior, runtime configuration, hidden database triggers, old clients, production data weirdness, and business context outside the repository.

That's why the best workflow combines AI with old-school engineering habits:

  • read the code
  • protect behavior with tests
  • refactor in small commits
  • review diffs carefully
  • run the suite
  • verify risky flows manually

Editorial split-screen titled Rewrite vs Refactor. Left panel Risky Rewrite shows a bulldozer demolishing a Legacy System building with bullets High Risk, Big Bang Disruption, Unknown Outcomes. Right panel Safe Refactor shows engineers reinforcing and upgrading the same building floor by floor with bullets Lower Risk, Incremental Changes, Continuous Value, Known Outcomes. Cream paper texture with black ink lines and red and teal accents.

Final Thought

Claude Code changes legacy refactoring because it gives you a faster way to understand the system. But understanding is still the goal. Don't use Claude to blindly rewrite old code; use it to investigate, explain, test, compare, and document. Then refactor slowly.

That's how you improve legacy systems without turning cleanup into a production incident.