{"slug": "i-didn-t-let-claude-upgrade-my-java-app-i-let-it-work-for-me", "title": "I Didn't Let Claude Upgrade My Java App. I Let It Work for Me.", "summary": "A developer used Claude Code to migrate a small legacy Spring Boot 2.x service from Java 8 to Java 21 and Spring Boot 3.3.5, splitting the work by area of change rather than by Java version and enforcing a rule that the agent writing code never grades it. The workflow used a Migrator subagent with fresh-context Verifier sessions, a MIGRATION.md checklist with scope, acceptance criteria and four status marks, plus saved API request/response replays and auth checks before committing. The developer warns that keeping the build red for most of the migration is risky on large monoliths and recommends running the checklist per module in dependency order.", "body_md": "Confession first: I'm rusty in Java. Years of frontend work will do that to you.\n\nAnd yet there I was, staring at a small legacy Spring Boot 2.x service still on Java 8. Untouched dependencies, patchy tests, and every \"small change\" came with a knot in my stomach. The target: Java 21, Spring Framework 6 and Spring Boot 3.3.5.\n\nI did it with Claude Code. This isn't a Java upgrade guide. It's how I let AI do the heavy lifting while keeping control firmly in my hands.\n\nI asked Claude how to do this properly. The answer was very correct and very by the book: hop through Java 11 and 17, run OpenRewrite recipes, plus five specialist reviewer agents.\n\nAll sensible. All a lot. Hopping through 11 and 17 is wasted effort when you're landing on 21 anyway. Five reviewers for one small app means more time managing agents than migrating code.\n\nClaude did hold one line, and it was right: split the work by *area of change*, not by Java version. The real risk is Spring Boot 3 landing jakarta, Security 6 and Hibernate 6 all at once. So here's what I did instead. Less by-the-book, more by-the-human.\n\nThe rule: **the agent that writes the code never grades it.** A subagent starts with a fresh context, so it doesn't inherit the Migrator's reasoning or its excuses. It's the colleague who wasn't in the room when you wrote that \"temporary\" hack.\n\nI never typed \"upgrade this to Java 21.\" The Migrator inventoried the codebase in plan mode and proposed a checklist. Once I approved it, it went into `MIGRATION.md`.\n\nProblem: I'm rusty, remember? I couldn't fully judge the plan. Is `WebSecurityConfigurerAdapter` really gone, or did Claude just not like it? So I opened a fresh Claude session (and yes, asked Gemini too) and grilled them on Spring Boot 3 breaking changes. AI fact-checking AI, for a language I used to know. It works, because a fresh session has no stake in the plan being right.\n\nThe order:\n\n`WebSecurityConfigurerAdapter` to `SecurityFilterChain`\nEach item gets a scope, acceptance criteria and a \"done when\":\n\n```\n### Item 3: javax to jakarta\nScope: src/** (Java, properties, XML)\nAcceptance:\n- No javax.* imports, except JDK packages (javax.crypto, javax.sql, ...)\n- javax.persistence property keys migrated too\n- No logic changes\nDone when: no compile errors or removal warnings in scope,\n  except errors owned by later items (springdoc: 4, Security: 5,\n  Hibernate types: 6, web: 7)\nStatus: [ ]\n```\n\nAnd four marks, because \"done\", \"verified\" and \"approved\" are different claims:\n\n`[ ]` to do, or sent back`[x]` Migrator says it's done`[v]` Verifier passed it, waiting for me`[a]` I approved it\nBefore item 1, I tagged the Java 8 code `pre-migration` and saved some real API requests and responses. Then every item went through this:\n\nIf a re-check finds an earlier item broke, that item goes back to `[ ]` and needs my go again.\n\nBefore calling it done: one final Verifier run over everything since `pre-migration`, a green build with all tests passing, a replay of the saved API calls, and protected endpoints hit with and without auth. Only then did anything go into git.\n\nIn my run, the build was red for most of the migration. Spring Boot 3 changes so much at once that nothing compiles until everything is done. On my small app, I judged only the end result. **Don't copy that bit.** On a big monolith it's walking blind through a forest: hundreds of errors, and no idea which ones the last change caused.\n\nThe rule I'd teach instead: **the build may be red, but an item isn't done while its files have compile errors or removal warnings that it, or an earlier item, owns.**\n\n```\n<compilerArgs>\n  <arg>-Xmaxerrs</arg><arg>10000</arg>\n  <arg>-Xmaxwarns</arg><arg>10000</arg>\n</compilerArgs>\n```\n\nOn a multi-module project, run the checklist once per module, in dependency order. Within a module the owned-errors rule still applies. A module is finished only when `mvn -pl <module> -am compile` is fully clean, because everything downstream compiles against it.\n\nOne external service needed a `DELETE` with a body. HttpClient 4's `HttpDelete` can't carry one, so years ago someone wrote a workaround: a custom `RestTemplate` request factory that swapped in an `HttpEntityEnclosingDeleteRequest` for every `DELETE`.\n\nSpring Framework 6 dropped HttpClient 4, so the whole client setup moved to HttpClient 5: TLS and pool limits go to a connection manager, and timeouts become `Timeout` values. The easy path was to port the workaround too. The Verifier pointed out that in HttpClient 5, `HttpDelete` takes a body natively. The override could just go.\n\nThe funny part? Spring's own request factory has handled `DELETE` bodies since 4.1, so that override was probably dead weight for years. The compiler would have told me the old classes were gone. It would never have told me the override wasn't needed.\n\nThe Migrator's rules, in `CLAUDE.md`:\n\n```\nTarget: Java 21, Spring Boot 3.3.5 (Spring Framework 6)\nBuild: ./mvnw clean verify\nPlan: MIGRATION.md\n\n- Work one checklist item at a time\n- Never delete, skip or weaken a test\n- An item is done only when its scope compiles clean\n  (every remaining error or removal warning owned by a later item)\n- Done: mark it [x] and call migration-verifier\n- FAIL: set it back to [ ] and fix only the findings\n- 3 FAILs on the same item: stop and ask me\n- Regression in an earlier item: send that item back to [ ]\n  and fix it before continuing\n- PASS: mark it [v] and WAIT for me\n- When I say go: mark it [a], commit on the migration branch\n  (a red build is fine), and start the next item\n```\n\nThe Verifier, in `.claude/agents/migration-verifier.md`:\n\n```\n---\nname: migration-verifier\ndescription: Verifies every checked item in MIGRATION.md.\ntools: Read, Grep, Glob, Bash\nmodel: opus   # or fable\n---\nReview like a skeptical senior engineer. Assume the change is wrong\nand look for evidence. Do not modify source files or git state\n(build output in target/ is fine).\n\n- The item just marked [x]: review its changes since the last commit (git diff HEAD),\n  including new untracked files. Check scope, acceptance criteria,\n  test integrity and behaviour changes.\n- Compile so every error is reported. The overall build may be red,\n  but this item's scope must be clean: list each compile error or\n  removal warning in its files with file:line and the later item\n  that owns it. Unowned, or owned by this or an earlier item: FAIL.\n- Items marked [v] or [a]: re-check their acceptance criteria\n  against the current code. Anything broken is a regression.\n\nAlways flag tests that were deleted, @Disabled or weakened.\nEvery finding needs file:line and evidence.\nEnd with VERDICT: PASS or FAIL.\n```\n\nHonest note: I only committed once, at the very end. The per-item commit in `CLAUDE.md` is the fix I'd make next time. It's also what lets the Verifier see just the newest item instead of everything at once.\n\nTo reuse it everywhere, move the workflow rules into a skill at `~/.claude/skills/verified-upgrade/SKILL.md`, keep the Verifier in `~/.claude/agents/`, and start any upgrade with `/verified-upgrade`. The Verifier stays a subagent because it needs its own fresh context. Commit both into a repo's `.claude/` folder to share them with your team. Nothing here is Java-specific, and as a frontend person, that's the part I'm most excited about.\n\n`git diff` misses new files.`git status`.` src/` edits and git-changing commands with a PreToolUse hook in the Verifier's own frontmatter, and let it write build output to `target/`.` javax` grep) still matter. On a big codebase, OpenRewrite is the better tool for the mechanical jakarta pass.\nAnd yes, Spring Boot 4 is out, and 3.3 is already past its open-source support window. Getting from 2 to 3 was the big jump. Staying current from here (the latest 3.5 as a stepping stone, then 4) is the easy part, and that's exactly what the skill is for.\n\nIs this the best way? Honestly, I don't know. It's my way, and it worked. If you've done it better or can spot a hole, tell me. I'd love to steal your ideas.\n\nThe agent that writes the code never grades it. And neither of them moves on without me.", "url": "https://wpnews.pro/news/i-didn-t-let-claude-upgrade-my-java-app-i-let-it-work-for-me", "canonical_source": "https://dev.to/koushikr/i-didnt-let-claude-upgrade-my-java-app-i-let-it-work-for-me-7d2", "published_at": "2026-10-12 15:38:46+00:00", "updated_at": "2026-10-12 15:51:01.453773+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "large-language-models"], "entities": ["Claude Code", "Anthropic", "Java 21", "Spring Boot 3.3.5", "Spring Framework 6", "OpenRewrite", "Gemini", "Hibernate 6"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/i-didn-t-let-claude-upgrade-my-java-app-i-let-it-work-for-me", "markdown": "https://wpnews.pro/news/i-didn-t-let-claude-upgrade-my-java-app-i-let-it-work-for-me.md", "text": "https://wpnews.pro/news/i-didn-t-let-claude-upgrade-my-java-app-i-let-it-work-for-me.txt", "jsonld": "https://wpnews.pro/news/i-didn-t-let-claude-upgrade-my-java-app-i-let-it-work-for-me.jsonld"}}