# I Didn't Let Claude Upgrade My Java App. I Let It Work for Me.

> Source: <https://dev.to/koushikr/i-didnt-let-claude-upgrade-my-java-app-i-let-it-work-for-me-7d2>
> Published: 2026-10-12 15:38:46+00:00

Confession first: I'm rusty in Java. Years of frontend work will do that to you.

And 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.

I 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.

I 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.

All 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.

Claude 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.

The 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.

I 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`.

Problem: 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.

The order:

`WebSecurityConfigurerAdapter` to `SecurityFilterChain`
Each item gets a scope, acceptance criteria and a "done when":

```
### Item 3: javax to jakarta
Scope: src/** (Java, properties, XML)
Acceptance:
- No javax.* imports, except JDK packages (javax.crypto, javax.sql, ...)
- javax.persistence property keys migrated too
- No logic changes
Done when: no compile errors or removal warnings in scope,
  except errors owned by later items (springdoc: 4, Security: 5,
  Hibernate types: 6, web: 7)
Status: [ ]
```

And four marks, because "done", "verified" and "approved" are different claims:

`[ ]` to do, or sent back`[x]` Migrator says it's done`[v]` Verifier passed it, waiting for me`[a]` I approved it
Before item 1, I tagged the Java 8 code `pre-migration` and saved some real API requests and responses. Then every item went through this:

If a re-check finds an earlier item broke, that item goes back to `[ ]` and needs my go again.

Before 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.

In 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.

The 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.**

```
<compilerArgs>
  <arg>-Xmaxerrs</arg><arg>10000</arg>
  <arg>-Xmaxwarns</arg><arg>10000</arg>
</compilerArgs>
```

On 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.

One 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`.

Spring 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.

The 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.

The Migrator's rules, in `CLAUDE.md`:

```
Target: Java 21, Spring Boot 3.3.5 (Spring Framework 6)
Build: ./mvnw clean verify
Plan: MIGRATION.md

- Work one checklist item at a time
- Never delete, skip or weaken a test
- An item is done only when its scope compiles clean
  (every remaining error or removal warning owned by a later item)
- Done: mark it [x] and call migration-verifier
- FAIL: set it back to [ ] and fix only the findings
- 3 FAILs on the same item: stop and ask me
- Regression in an earlier item: send that item back to [ ]
  and fix it before continuing
- PASS: mark it [v] and WAIT for me
- When I say go: mark it [a], commit on the migration branch
  (a red build is fine), and start the next item
```

The Verifier, in `.claude/agents/migration-verifier.md`:

```
---
name: migration-verifier
description: Verifies every checked item in MIGRATION.md.
tools: Read, Grep, Glob, Bash
model: opus   # or fable
---
Review like a skeptical senior engineer. Assume the change is wrong
and look for evidence. Do not modify source files or git state
(build output in target/ is fine).

- The item just marked [x]: review its changes since the last commit (git diff HEAD),
  including new untracked files. Check scope, acceptance criteria,
  test integrity and behaviour changes.
- Compile so every error is reported. The overall build may be red,
  but this item's scope must be clean: list each compile error or
  removal warning in its files with file:line and the later item
  that owns it. Unowned, or owned by this or an earlier item: FAIL.
- Items marked [v] or [a]: re-check their acceptance criteria
  against the current code. Anything broken is a regression.

Always flag tests that were deleted, @Disabled or weakened.
Every finding needs file:line and evidence.
End with VERDICT: PASS or FAIL.
```

Honest 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.

To 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.

`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.
And 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.

Is 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.

The agent that writes the code never grades it. And neither of them moves on without me.
