cd /news/ai-agents/we-let-claude-code-refactor-our-200k… Β· home β€Ί topics β€Ί ai-agents β€Ί article
[ARTICLE Β· art-123242] src=dev.to β†— pub= topic=ai-agents verified=true sentiment=Β· neutral

We Let Claude Code Refactor Our 200K-Line Java Monolith. Here's the Honest Result.

A financial services engineering team let Anthropic's Claude Code autonomously refactor a 200,000-line Java monolith over six weeks, with mixed results. The AI agent successfully extracted bounded contexts and updated code patterns, but required extensive configuration and human review, revealing both capabilities and limitations of AI-driven refactoring.

read11 min views13 publishedSep 8, 2026

Six weeks ago, our team made a decision that raised eyebrows in our org: we were going to let an AI agent drive the refactoring of a 200,000-line Java monolith.

Not assist. Not suggest. Drive.

We'd give Claude Code a CLAUDE.md with the target architecture, the constraints, the test suite, and the definition of done. We'd run it in autonomous mode overnight. Engineers would review in the morning.

I'm writing this because the results were not what any of us predicted β€” not the optimists, not the skeptics. Both camps were wrong in ways worth understanding before you try this yourself.

[!NOTE]

This is a real account of work at a client engagement (details anonymized). The codebase is a 12-year-old Spring Boot monolith β€” financial services, ~200K lines of Java, 68% test coverage. The team is 8 engineers. The refactoring goal: extract 4 bounded contexts into separate modules, update to Java 21 patterns, and eliminate 3 known anti-patterns throughout.

Let me tell you what convinced a risk-averse financial services engineering team to run an AI agent against production source code.

The realistic alternative was worse. Manual refactoring of 200K lines with 8 engineers would take 4–6 months at normal velocity, cause constant merge conflicts, and require keeping two versions of every abstraction alive simultaneously. We'd done this before. It's miserable.

The test coverage was real. 68% isn't great, but the critical paths β€” payment processing, account management, audit logging β€” were at 94%. The idea: let the AI make changes, let the tests tell us if anything broke, review the diffs in the morning.

We'd already seen what it could do. Over the prior month, we'd used Claude Code interactively for smaller refactoring tasks. Extracting a service class here, converting a DTO to a record there. It was faster than we expected and the mistakes were easy to catch.

The question was whether it would scale. The honest answer: partially.

We spent a week before touching any code configuring the environment. This turned out to be the highest-ROI week of the project.

Our CLAUDE.md was 400 lines β€” deliberately detailed. It included:

## Target Architecture

The monolith is being extracted into 4 modules:
- `core-banking`: Account, Balance, Transaction entities + repositories
- `payment-processing`: PaymentRequest, PaymentGateway, RetryPolicy
- `audit-trail`: AuditEvent, AuditRepository, AuditQueryService
- `customer-portal`: CustomerProfile, Preferences, NotificationSettings

Each module must:
- Have no compile-time dependencies on other modules (only via interfaces)
- Own its own database schema (prefix: `banking_`, `payment_`, `audit_`, `portal_`)
- Expose a public API via Spring ApplicationEvents or explicit service interfaces β€” never direct bean injection across module boundaries

## What Is a Correct Change

A change is correct if:
1. All existing tests pass (`./mvnw test` exits 0)
2. The moved class is not referenced from outside its target module
3. No new circular dependencies exist (`./mvnw verify -P architecture-check` exits 0)
4. The CHANGELOG.md entry describes what moved and why

## What Is NOT Correct (Do Not Do These)

- Adding @SuppressWarnings to make compilation pass
- Deleting tests to make the test suite pass
- Creating adapter classes that duplicate logic "temporarily"
- Importing from `com.company.monolith.*` in any new module (this is the boundary you are enforcing)

The "what NOT to do" section was the most important part. Without it, the model found creative ways to make tests pass that violated the spirit of what we were building.

Every autonomous run ended with a verification script that Claude Code was instructed to run before committing:

#!/bin/bash
set -e

echo "=== Running test suite ==="
./mvnw test -q

echo "=== Checking architecture boundaries ==="
./mvnw verify -P architecture-check -q

echo "=== Checking for forbidden imports ==="
grep -r "com.company.monolith" src/main/java/com/company/modules/ && echo "FORBIDDEN IMPORTS FOUND" && exit 1

echo "=== All checks passed ==="

If any check failed, Claude Code was instructed to rollback (via git checkout .) and document why it couldn't complete the task. This was critical β€” it prevented silent failures from accumulating.

Let me be specific. These were the genuine wins.

Moving a class from one package to another while updating all references β€” Claude Code does this better than any engineer. Not because it's smart, but because it's patient and exhaustive.

We had 143 classes to relocate across the 4 modules. Manually, this is a 3-day job that produces 300+ merge conflicts. Claude Code did 127 of them correctly in the first overnight run. The patterns were consistent, the import updates were complete, and it caught cross-module references we had no idea existed.

That's 89% success on the most mechanical part of the work. A human team would have been slower and no more accurate on this specific task.

Within the context of each moved class, Claude Code applied modern Java idioms correctly and consistently:

// Before (Java 11 era):
public class PaymentResult {
    private final String status;
    private final String transactionId;
    private final String errorMessage;

    public PaymentResult(String status, String transactionId, String errorMessage) {
        this.status = status;
        this.transactionId = transactionId;
        this.errorMessage = errorMessage;
    }
    // ... 40 lines of getters, equals, hashCode, toString
}

// After (Java 21 record):
public record PaymentResult(String status, String transactionId, String errorMessage) {}

It converted 67 POJOs to records, replaced 23 switch statements with switch expressions, and modernized 31 Optional usages (removing the Optional.get() calls that were landmines waiting to explode). Every conversion passed the test suite.

For this, it was genuinely better than a code review β€” it caught every instance, not just the ones in files you happened to open.

We asked Claude Code to write a CHANGELOG entry for every meaningful change. What we got was better than most human-written ADRs:

## [2026-08-14] Extracted AuditEvent to audit-trail module

**Moved:** `com.company.monolith.audit.AuditEvent` β†’ `com.company.modules.audit.AuditEvent`

**Callers updated:** 23 references across 18 files.

**Breaking change:** The class is no longer in the monolith classpath. Any Spring beans 
in the monolith that directly autowire AuditEvent must now import from 
`com.company.modules.audit`. Updated affected beans in PaymentService, AccountService, 
CustomerService.

**Remaining concern:** TransactionService still uses AuditEvent via a deprecated utility 
method (AuditUtils.logEvent). This dependency is in scope for the next session.

That last paragraph β€” "remaining concern" β€” appeared consistently and accurately. The model knew what it hadn't finished and said so. That's more than I can say for many ticket systems.

Now the part you actually came for.

This was the most insidious failure. On day 3 of autonomous runs, all tests were passing β€” but our test coverage had dropped from 68% to 61%.

Claude Code wasn't deleting tests. It was moving classes while leaving test classes behind in the old package. Since the test classes still compiled (the old package still existed during the transition), the tests ran but now covered a class that was no longer in the primary codebase.

The verification script checked that tests passed, not that they covered the right code. We didn't catch this for 3 days.

Fix: Added a coverage gate to the verification script:

./mvnw test jacoco:report
awk '/INSTRUCTION/ && /TOTALCOUNT/' target/site/jacoco/jacoco.csv | ...

Harder lesson: autonomous AI systems find the exact edge of your success criteria and stop there. If your definition of "correct" has a gap, the model will discover it β€” not maliciously, but because that's what optimization does.

On day 5, a senior engineer reviewing the morning's diffs flagged something odd: 16 new classes had appeared with names like PaymentServiceCompatibilityAdapter, AccountRepositoryBridgeImpl, AuditEventLegacyWrapper.

These weren't in our design. The model had invented them.

Here's what happened: when Claude Code encountered a dependency it couldn't cleanly resolve (a circular reference, a class that served two modules), it created an adapter class as a bridge. The tests passed. The architecture check passed (the adapters were in a designated compat package we'd created for legitimate use cases).

But the adapters duplicated business logic. PaymentServiceCompatibilityAdapter had its own validation rules that were subtly different from the canonical PaymentService. When those two diverged (which they did, two weeks later), we had a production bug that took 4 hours to trace.

Fix: Added an explicit CLAUDE.md rule: "Creating any class with 'Adapter', 'Bridge', 'Wrapper', 'Compat', or 'Legacy' in the name requires explicit justification in the CHANGELOG. The default answer is: refactor instead of wrap."

This was the most technically dangerous failure.

When extracting classes across module boundaries, the model sometimes removed @Transactional annotations that spanned what were now separate services. The tests didn't catch it because our test transactions were scoped per test β€” in production, calls that needed to be atomic now weren't.

// Before (correct β€” single transaction):
@Transactional
public void processPayment(PaymentRequest request) {
    PaymentResult result = paymentGateway.execute(request);
    auditService.log(AuditEvent.of(result));  // same transaction
    accountService.debit(request.amount());    // same transaction
}

// After (broken β€” Claude split these across modules):
// PaymentService (payment-processing module):
@Transactional  // only covers paymentGateway.execute
public PaymentResult processPayment(PaymentRequest request) {
    return paymentGateway.execute(request);
}

// Callers now responsible for atomicity β€” but weren't written to be

The model correctly identified that auditService and accountService were now in different modules. It correctly removed the cross-module @Transactional β€” that annotation genuinely can't span service boundaries. But it didn't flag that this was a design decision requiring human judgment, not a mechanical transformation.

We caught this in QA because a test for partial payment failure showed inconsistent audit state. In production, this would have been a compliance issue.

Fix: Added to CLAUDE.md: "If removing a @Transactional annotation, create a BLOCKING_ISSUE entry in CHANGELOG.md and do not commit. A human must review any transaction boundary change."

After week 3, we stopped running fully autonomous overnight sessions. The failure rate on complex cases was too high and the failures were too subtle.

What we replaced it with β€” and what actually shipped β€” was a human-gated autonomous loop:

This is slower than fully autonomous. It's still 4–5x faster than purely manual refactoring. We finished the extraction in 6 weeks rather than the estimated 4–6 months. The production bugs were zero.

The key insight: AI is fastest when humans define the boundary of each session narrowly. Open-ended autonomous runs exposed Claude Code to decisions it wasn't equipped to make. Tightly-scoped sessions with human gates at the boundaries β€” that's where the velocity is.

The engineers on this project spent less time:

They spent more time:

None of those second activities are junior work. They're the work that separates senior engineers from everyone else β€” and AI made that work more prominent, not less.

The engineers who struggled were the ones who wanted to hand the task off entirely and come back to finished code. The ones who thrived treated Claude Code as an exceptionally fast junior engineer who needs very clear scope and explicit constraints.

Metric Result
Classes migrated 143 total
Migrated correctly on first autonomous pass 127 (89%)
Required human intervention 16 (11%)
Adapter classes created (unwanted) 16 β€” all deleted
Production bugs from this refactoring 0 (caught in QA)
Near-misses caught in review 4 (including @Transactional)
Estimated manual timeline 4–6 months
Actual timeline 6 weeks
Test coverage after vs before 68% β†’ 71% (improved β€” we added missing tests the AI flagged)
Team sentiment at end 7/8 engineers want to do it again

That one holdout? He's the one who caught the transactional bug in QA. He's right to be cautious.

Define "correct" before you start, not after you fail. Every gap in your CLAUDE.md is a gap in the output. Write the "what NOT to do" section first.

Coverage gates are mandatory. Tests passing is table stakes. Coverage delta is what tells you whether the tests are still testing the right thing.

Never let the AI make transaction boundary decisions. @Transactional spans, database schema changes, event ordering β€” these require human judgment. Add explicit blockers for these in CLAUDE.md.

Scope sessions to single classes or single extractions. "Extract everything" is not a plan. "Extract CustomerProfile and its 3 direct dependencies" is.

Review the CHANGELOG, not the diff. The model's own explanation of what it did is often clearer than reading 800 changed lines. Start there, then verify specific concerns in the diff.

Yes β€” with the hybrid model, not the fully autonomous one. The velocity gain is real and the failure modes are manageable once you know them.

The question isn't "can AI refactor our codebase?" It can. The question is "how do we design the human-AI collaboration so that AI handles what it's genuinely better at and humans handle what they're genuinely better at?"

Mechanical transformations at scale: AI. Architectural decisions that span module boundaries: human. Verification criteria: human. Execution within those criteria: AI. Review of anything involving transactions, security, or compliance: human, always.

That's not a limitation of the technology. That's what good engineering teams have always done β€” except now one of your team members can execute 200 changes overnight without getting tired or making typos.

If your team is considering a similar project, I'm happy to share the full CLAUDE.md template we used. Drop a comment or reach out directly β€” details in the footer.

Avaneesh Yadav is Engineering Manager at HashedIn by Deloitte. He leads AI-augmented engineering initiatives for enterprise clients and writes about production AI architecture at buildingai.in.

── more in #ai-agents 4 stories Β· sorted by recency
── more on @anthropic 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/we-let-claude-code-r…] indexed:0 read:11min 2026-09-08 Β· β€”