{"slug": "we-let-claude-code-refactor-our-200k-line-java-monolith-here-s-the-honest-result", "title": "We Let Claude Code Refactor Our 200K-Line Java Monolith. Here's the Honest Result.", "summary": "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.", "body_md": "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.\n\nNot assist. Not suggest. **Drive.**\n\nWe'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.\n\nI'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.\n\n[!NOTE]\n\nThis 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.\n\nLet me tell you what convinced a risk-averse financial services engineering team to run an AI agent against production source code.\n\n**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.\n\n**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.\n\n**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.\n\nThe question was whether it would scale. **The honest answer: partially.**\n\nWe spent a week before touching any code configuring the environment. This turned out to be the highest-ROI week of the project.\n\nOur CLAUDE.md was 400 lines — deliberately detailed. It included:\n\n```\n## Target Architecture\n\nThe monolith is being extracted into 4 modules:\n- `core-banking`: Account, Balance, Transaction entities + repositories\n- `payment-processing`: PaymentRequest, PaymentGateway, RetryPolicy\n- `audit-trail`: AuditEvent, AuditRepository, AuditQueryService\n- `customer-portal`: CustomerProfile, Preferences, NotificationSettings\n\nEach module must:\n- Have no compile-time dependencies on other modules (only via interfaces)\n- Own its own database schema (prefix: `banking_`, `payment_`, `audit_`, `portal_`)\n- Expose a public API via Spring ApplicationEvents or explicit service interfaces — never direct bean injection across module boundaries\n\n## What Is a Correct Change\n\nA change is correct if:\n1. All existing tests pass (`./mvnw test` exits 0)\n2. The moved class is not referenced from outside its target module\n3. No new circular dependencies exist (`./mvnw verify -P architecture-check` exits 0)\n4. The CHANGELOG.md entry describes what moved and why\n\n## What Is NOT Correct (Do Not Do These)\n\n- Adding @SuppressWarnings to make compilation pass\n- Deleting tests to make the test suite pass\n- Creating adapter classes that duplicate logic \"temporarily\"\n- Importing from `com.company.monolith.*` in any new module (this is the boundary you are enforcing)\n```\n\nThe \"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.\n\nEvery autonomous run ended with a verification script that Claude Code was instructed to run before committing:\n\n``` bash\n#!/bin/bash\nset -e\n\necho \"=== Running test suite ===\"\n./mvnw test -q\n\necho \"=== Checking architecture boundaries ===\"\n./mvnw verify -P architecture-check -q\n\necho \"=== Checking for forbidden imports ===\"\ngrep -r \"com.company.monolith\" src/main/java/com/company/modules/ && echo \"FORBIDDEN IMPORTS FOUND\" && exit 1\n\necho \"=== All checks passed ===\"\n```\n\nIf 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.\n\nLet me be specific. These were the genuine wins.\n\nMoving 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.\n\nWe 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.\n\nThat'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.\n\nWithin the context of each moved class, Claude Code applied modern Java idioms correctly and consistently:\n\n```\n// Before (Java 11 era):\npublic class PaymentResult {\n    private final String status;\n    private final String transactionId;\n    private final String errorMessage;\n\n    public PaymentResult(String status, String transactionId, String errorMessage) {\n        this.status = status;\n        this.transactionId = transactionId;\n        this.errorMessage = errorMessage;\n    }\n    // ... 40 lines of getters, equals, hashCode, toString\n}\n\n// After (Java 21 record):\npublic record PaymentResult(String status, String transactionId, String errorMessage) {}\n```\n\nIt 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.\n\nFor this, it was genuinely better than a code review — it caught every instance, not just the ones in files you happened to open.\n\nWe asked Claude Code to write a CHANGELOG entry for every meaningful change. What we got was better than most human-written ADRs:\n\n```\n## [2026-08-14] Extracted AuditEvent to audit-trail module\n\n**Moved:** `com.company.monolith.audit.AuditEvent` → `com.company.modules.audit.AuditEvent`\n\n**Callers updated:** 23 references across 18 files.\n\n**Breaking change:** The class is no longer in the monolith classpath. Any Spring beans \nin the monolith that directly autowire AuditEvent must now import from \n`com.company.modules.audit`. Updated affected beans in PaymentService, AccountService, \nCustomerService.\n\n**Remaining concern:** TransactionService still uses AuditEvent via a deprecated utility \nmethod (AuditUtils.logEvent). This dependency is in scope for the next session.\n```\n\nThat 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.\n\nNow the part you actually came for.\n\nThis 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%.\n\nClaude 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.\n\nThe verification script checked that tests *passed*, not that they *covered* the right code. We didn't catch this for 3 days.\n\n**Fix:** Added a coverage gate to the verification script:\n\n```\n./mvnw test jacoco:report\n# Fail if overall coverage drops below 65%\nawk '/INSTRUCTION/ && /TOTALCOUNT/' target/site/jacoco/jacoco.csv | ...\n```\n\nHarder 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.\n\nOn day 5, a senior engineer reviewing the morning's diffs flagged something odd: 16 new classes had appeared with names like `PaymentServiceCompatibilityAdapter`, `AccountRepositoryBridgeImpl`, `AuditEventLegacyWrapper`.\n\nThese weren't in our design. The model had invented them.\n\nHere'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).\n\nBut 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.\n\n**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.\"\n\nThis was the most technically dangerous failure.\n\nWhen 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.\n\n```\n// Before (correct — single transaction):\n@Transactional\npublic void processPayment(PaymentRequest request) {\n    PaymentResult result = paymentGateway.execute(request);\n    auditService.log(AuditEvent.of(result));  // same transaction\n    accountService.debit(request.amount());    // same transaction\n}\n\n// After (broken — Claude split these across modules):\n// PaymentService (payment-processing module):\n@Transactional  // only covers paymentGateway.execute\npublic PaymentResult processPayment(PaymentRequest request) {\n    return paymentGateway.execute(request);\n}\n\n// Callers now responsible for atomicity — but weren't written to be\n```\n\nThe 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.\n\nWe caught this in QA because a test for partial payment failure showed inconsistent audit state. In production, this would have been a compliance issue.\n\n**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.\"\n\nAfter week 3, we stopped running fully autonomous overnight sessions. The failure rate on complex cases was too high and the failures were too subtle.\n\nWhat we replaced it with — and what actually shipped — was a **human-gated autonomous loop**:\n\nThis 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.\n\nThe 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.\n\nThe engineers on this project spent less time:\n\nThey spent more time:\n\nNone 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.\n\nThe 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.\n\n| Metric | Result | \n|---|---|\n| Classes migrated | 143 total | \n| Migrated correctly on first autonomous pass | 127 (89%) | \n| Required human intervention | 16 (11%) | \n| Adapter classes created (unwanted) | 16 — all deleted | \n| Production bugs from this refactoring | 0 (caught in QA) | \n| Near-misses caught in review | 4 (including @Transactional) | \n| Estimated manual timeline | 4–6 months | \n| Actual timeline | 6 weeks | \n| Test coverage after vs before | 68% → 71% (improved — we added missing tests the AI flagged) | \n| Team sentiment at end | 7/8 engineers want to do it again | \n\nThat one holdout? He's the one who caught the transactional bug in QA. He's right to be cautious.\n\n**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.\n\n**Coverage gates are mandatory.** Tests passing is table stakes. Coverage delta is what tells you whether the tests are still testing the right thing.\n\n**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.\n\n**Scope sessions to single classes or single extractions.** \"Extract everything\" is not a plan. \"Extract CustomerProfile and its 3 direct dependencies\" is.\n\n**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.\n\nYes — with the hybrid model, not the fully autonomous one. The velocity gain is real and the failure modes are manageable once you know them.\n\nThe 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?\"\n\nMechanical 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.\n\nThat'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.\n\n*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.*\n\n*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.*", "url": "https://wpnews.pro/news/we-let-claude-code-refactor-our-200k-line-java-monolith-here-s-the-honest-result", "canonical_source": "https://dev.to/avaneeshyadav/we-let-claude-code-refactor-our-200k-line-java-monolith-heres-the-honest-result-3fdf", "published_at": "2026-09-08 11:46:23+00:00", "updated_at": "2026-09-08 12:02:14.670189+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "artificial-intelligence"], "entities": ["Anthropic", "Claude Code"], "alternates": {"html": "https://wpnews.pro/news/we-let-claude-code-refactor-our-200k-line-java-monolith-here-s-the-honest-result", "markdown": "https://wpnews.pro/news/we-let-claude-code-refactor-our-200k-line-java-monolith-here-s-the-honest-result.md", "text": "https://wpnews.pro/news/we-let-claude-code-refactor-our-200k-line-java-monolith-here-s-the-honest-result.txt", "jsonld": "https://wpnews.pro/news/we-let-claude-code-refactor-our-200k-line-java-monolith-here-s-the-honest-result.jsonld"}}