This was a modernization project on a Java service that had been stable for years. The service processed billing events, exposed a few internal REST endpoints, and ran on Spring Boot 2.x. The goal was to move to Spring Boot 3, update dependencies, and clean up test coverage around the parts most likely to break.
I used GitHub Copilot inside IntelliJ for the migration. No agent loop, no autonomous edits, no “do the upgrade” prompt. Just chat, inline completion, and repeated small refactors.
That low-drama setup was exactly right for the project.
The service
The codebase was not huge:
- about 21k lines of Java
- Spring Boot 2.7
- Java 11 moving to Java 17
- Maven build
- roughly 68% line coverage
- 37 REST endpoints, mostly internal
- one Kafka consumer path that mattered more than the endpoints
The scary part was not code volume. It was semantic drift. Spring Boot 3 means the javax to jakarta move, dependency version changes, security configuration changes, and enough test behavior differences to make a simple upgrade noisy.
What Copilot was allowed to do
I did not ask Copilot to own the migration plan. I wrote the plan by hand:
- Move build to Java 17.
- Upgrade Spring Boot and dependency BOM.
- Fix compile errors.
- Migrate
javaximports tojakarta. - Update security configuration.
- Repair tests.
- Add coverage around billing-event edge cases.
- Run staging replay before production.
Copilot was used inside those steps for bounded work:
- explain a compiler error
- update a small class to the new API
- generate a parallel test case
- rewrite a deprecated assertion pattern
- summarize a dependency changelog section
The rule was simple: if the prompt needed more than one paragraph, I probably needed to think more before asking.
Where Copilot helped
The import migration. The javax to jakarta sweep is boring and error-prone. IDE refactors handled most of it, but Copilot helped on classes where imports were entangled with generated code or old validation annotations.
Test expansion. Billing systems have many small branch cases. Copilot was good at writing additional JUnit cases once I wrote the first one in the style I wanted. The useful prompt was not “write tests.” It was:
Add three more test cases following the existing pattern:
- duplicate billing event id
- event timestamp older than account creation
- valid event with missing optional metadata
Do not change production code.
That produced tests I could review quickly. A few assertions needed tightening, but the structure was right.
Small API refactors. Spring Security configuration changed enough that old examples were misleading. Copilot helped translate small pieces after I pointed it at the new pattern used in the repo.
Explaining unfamiliar errors. Some errors were Maven dependency-resolution noise. Copilot’s explanations were good enough to point me at the right dependency tree without sending me through several tabs of search results.
Where Copilot did not help
Migration ordering. The order of work mattered. If I had started by letting Copilot fix random compile errors, the diff would have mixed build changes, import changes, and behavior changes. The human plan kept the migration reviewable.
Security behavior. Copilot suggested a security configuration that compiled and passed simple tests but changed the behavior of unauthenticated requests on one internal endpoint. That would have been a production bug.
Security migrations need explicit behavioral tests before code changes. Copilot can make those tests faster to write, but it should not infer the rules.
Kafka replay validation. The final confidence came from replaying a staging copy of billing events through the upgraded service and comparing outputs. Copilot had nothing useful to add there. The evidence came from data.
The numbers
| Work area | Without Copilot estimate | Actual |
|---|---|---|
| Java 17 and build update | 1 day | 1 day |
| Spring Boot dependency upgrade | 2 days | 2 days |
| Compile-error cleanup | 4 days | 2.5 days |
| Security migration | 3 days | 3 days |
| Test repair and expansion | 6 days | 3.5 days |
| Staging replay and verification | 4 days | 4 days |
Total estimate was about 20 working days. Actual time was 13 working days.
The savings came from two places: repetitive compile-error cleanup and test expansion. The hard parts, security behavior and staging verification, did not get much faster.
The test pattern that paid off
Before changing security configuration, I wrote a small matrix:
| Endpoint type | Anonymous | User token | Service token |
|---|---|---|---|
| health | allowed | allowed | allowed |
| internal billing read | denied | denied | allowed |
| account summary | denied | allowed | allowed |
| admin override | denied | denied | admin only |
Then I asked Copilot to generate JUnit parameterized tests from that table using the existing security test helper.
The generated test was not perfect. It used one wrong role name and duplicated setup code. But it was close enough that fixing it took 10 minutes instead of writing the whole matrix from scratch.
Those tests caught the bad security suggestion later. That is the right relationship: AI helps create the guardrail, then the guardrail evaluates the AI’s code.
Review cost
Copilot’s output was small enough to review inline. That was the main advantage over agent-style tools for this project.
I never had a 2,000-line AI diff. Most accepted changes were:
- one test class
- one configuration class
- one model update
- one dependency section
That pacing kept review cost low. It also made it easy to discard suggestions. When a completion looked wrong, I ignored it. There was no sunk-cost feeling.
What I would do differently
I would start with the behavior matrix even earlier. The security migration was the riskiest part, and I should have written those tests before touching dependencies.
I would also keep a written migration log in the repo:
2026-05-02: Java 17 build passing
2026-05-03: Boot 3 compile errors reduced from 412 to 38
2026-05-06: security behavior matrix passing
2026-05-08: staging replay matched 99.98%; two expected differences documented
Copilot can summarize a diff, but it cannot replace a human-readable migration log that explains why decisions were made.
Verdict
Copilot was useful here because it stayed small. It made repetitive Java migration work faster without trying to own the project.
For Spring Boot modernization, I would not use Copilot as an autonomous agent. I would use it as a high-quality assistant for tests, API translations, and local explanations. That sounds modest, but on a 13-day migration, modest help was enough to save about a week.