Published on

Migrating a Fleet of Spring Boot APIs with a Shared Parent POM

Authors
  • avatar
    Name
    Motions Technologies
    Twitter

Migrating a Fleet of Spring Boot APIs with a Shared Parent POM

When you run more than a handful of Spring Boot microservices, version drift is not a theoretical risk — it is the default. One service sits on Boot 3.2, another on 3.5, Dockerfiles disagree on the JDK, and AWS SDK pins diverge. We recently took a food-delivery platform's API fleet (about twelve services) through a controlled upgrade to Spring Boot 4 and a modern JDK. The pattern that made it safe was a shared parent POM, the same idea banks use for governance.

The problem with per-service parents

Each API independently declared something like:

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.5.x</version>
</parent>

plus duplicated java.version, compiler settings, and AWS SDK BOM imports. That is how fleets accumulate skew: every upgrade becomes twelve copy-paste edits and twelve chances to miss a Dockerfile or CI JDK pin.

Introduce a company parent POM

We published a thin parent artifact (hosted on GitHub Packages) that sits between Spring Boot and the APIs:

spring-boot-starter-parent
company-api-parent  (Boot + Java + AWS BOM)
customer-api / order-api / payment-api / …

What the parent owns

  • Boot and Java versions (one place to bump)
  • AWS SDK for Java 2.x BOM in dependencyManagement
  • Compiler / Surefire / Spring Boot plugin defaults
  • Lombok annotation processing defaults

What the parent is not

  • Not a monorepo reactor — APIs stay independent git repos
  • Not forced shared libraries (those can come later)
  • Children may add service-specific deps; they should not re-pin Boot/Java without a documented exception

Two-step adoption (do not jump everything at once)

Parent 1.x — adopt without upgrading Boot

First publish a parent that still targets the current Boot line (for us, Boot 3.5.x). Point every API at it. Run Maven + existing smoke tests. You have aligned governance before you change runtime behavior.

Parent 2.x — Boot 4 + JDK bump together

Spring Boot 4.1 supports a specific JDK window (through Java 26 at the time we ran this). We shipped Boot 4 and the new JDK in the same parent bump so compile, Docker base images, and CI setup-java stay on one supported matrix.

Per API when moving to parent 2.x:

  1. Bump <parent><version>
  2. Switch Dockerfile to the matching Temurin base image
  3. Align GitHub Actions JDK
  4. Run local Maven on that JDK before push
  5. Deploy behind smoke tests

DynamoDB before Boot 4 for heavy services

Several services still used the unmaintained spring-data-dynamodb stack. That combination is a poor foundation for Boot 4. We migrated those APIs to the AWS SDK v2 Enhanced Client (modules only, versions from the parent BOM) before their Boot 4 bump.

Safer wave order we used:

  1. Pilot a low-risk API on parent 2.x (Boot 4 + new JDK)
  2. Roll Boot 4 to APIs without Dynamo legacy
  3. Migrate Dynamo-heavy APIs to SDK v2 Enhanced Client
  4. Boot 4 the remaining services — payment last, with a longer soak

Smoke tests are the promotion gate

We treated pre/post-deploy HTTP smoke (Hurl suites per API) as the hard gate: if smoke fails, the service does not stay promoted. That discipline mattered more than any single migration checklist item. OpenRewrite helped with mechanical API changes; humans still owned dual-write edges, auth, and payment paths.

Lessons worth stealing

  1. Parent POM first, runtime jump second — alignment without behavior change reduces blast radius.
  2. Clear blocked dependencies early — unmaintained Spring Data Dynamo adapters will own your schedule if you ignore them.
  3. Pilot → wave → critical path last — payment and order flows get the longest soak.
  4. Document the inventory — keep a living table of Boot/Java/SDK versions per service; it pays for itself on the next fleet bump.
  5. Non-LTS JDKs are fine for short windows — plan the next LTS parent bump as normal ops cadence, not a surprise fire drill.

Closing

Fleet upgrades fail when every service invents its own Spring Boot line. A published parent POM turns "upgrade twelve repos" into "publish parent 2.0.0, then bump twelve parent versions." Pair that with dependency cleanup, a pilot service, and ruthless smoke gates, and Boot 4 becomes an engineering project instead of a weekend gamble.