Validate Spring Boot Upgrades with Traffic Replay
TL;DR
Spring Boot version upgrades—whether moving from 2.x to 3.x, 3.x to 4.x, or even minor bumps like 3.2.5 to 3.3.1—regularly introduce subtle, breaking changes that unit and integration tests miss. JSON serialization shifts, autoconfiguration reordering, and transitive dependency conflicts can silently alter your API contract. In this post, you’ll learn how to use Speedscale traffic replay to record production traffic on your current Spring Boot version, replay it against the upgraded build, and catch every byte-level regression before users or downstream clients are impacted.
What makes Spring Boot upgrades more dangerous than they appear
Spring Boot’s convention-over-configuration philosophy and aggressive dependency management mean that a version bump can ripple through your application in unexpected ways. Release notes and migration guides cover the obvious breaking changes, but the silent ones—changes that don’t throw exceptions or fail tests—are the ones that reach production.
JSON and serialization drift
Real-world example: A team upgraded from Spring Boot 3.2.5 to 3.3.1—a minor, supposedly safe update. The upgrade pulled in a Jackson Databind bump (2.15.4 → 2.17.1), which introduced stricter handling of Java 8 Optional types. APIs that had been returning Optional values in JSON without explicit configuration suddenly started throwing InvalidDefinitionException at runtime: “Java 8 optional type java.util.Optional not supported by default.” The fix was simple once identified (register Jdk8Module), but production went down before anyone connected the dots. (See João Vieira’s post for the full account.)
Another documented incident: A Spring Boot minor upgrade changed JSON output in two ways—field names shifted from camelCase to snake_case, and numeric values like 1250.0 became 1250. Unit tests passed. Integration tests passed. But strict JSON schema validation at a partner integration rejected the responses, a mobile app crashed on deserialization, and a third client’s floating-point parser failed. The API contract exists in the bytes on the wire, not in your OpenAPI spec. If those bytes change, clients break. (See this Medium post for the full story.)
| Issue | Example | Root cause |
|---|---|---|
| Optional serialization fails | InvalidDefinitionException at runtime | Jackson Databind upgrade drops implicit JDK8 module |
| Field naming change | createdAt → created_at | Jackson property naming / config change |
| Numeric format change | 1250.0 → 1250 | Serialization default changes |
| Date format change | Timestamp vs ISO-8601 string | Jackson 2→3 or config drift |
| Null handling | null vs omitted field | @JsonInclude or module behavior change |
Autoconfiguration and transitive dependencies
Adding or upgrading a dependency can pull in transitive imports that trigger Spring Boot autoconfiguration you never explicitly wanted. A transitive spring-security-core dependency can enable security autoconfiguration and change request handling. Auto-configuration order matters: @AutoConfigureBefore and @AutoConfigureAfter control bean registration order, but dependency injection can instantiate beans in a different order, leading to subtle behavioral differences.
When upgrading Spring Boot, the managed dependency tree shifts. Libraries you didn’t touch get new versions. Autoconfigure JARs are refactored—Spring Boot 4 split a single 6.2 MB autoconfigure JAR into 47 focused modules—which can change which configurations match and in what order. Your app may start fine but behave differently under real traffic.
Removed and relocated APIs
Spring Boot 4 removes Undertow (Servlet 6.1 incompatibility), drops Pulsar Reactive, and eliminates embedded executable JAR launch scripts. Spring Boot 2→3 migrates javax.* to jakarta.* across the board—javax.persistence, javax.validation, javax.annotation.PostConstruct—requiring bulk import updates. Elasticsearch’s RestClient is replaced with Rest5Client. MockitoTestExecutionListener is removed; tests that relied on it can silently fail without the MockitoExtension annotation. Null-safety tooling migrates from spring-lang to JSpecify, causing compile failures or stricter runtime checks.
Compile-time breaks are obvious. Runtime differences in replacement libraries—behavioral quirks, edge cases, performance characteristics—are not.
Property and configuration changes
spring-boot-properties-migrator helps with renamed properties, but not every change is covered. Actuator security moved from management.security.* in 1.x to Spring Security integration in 2.x+. Health endpoint behavior, endpoint exposure, and role-based visibility have all evolved. Micrometer and OpenTelemetry modules have been reorganized in Spring Boot 4, with property naming changes. Configuration that “just worked” can silently change behavior across versions.
Why traffic replay is the right tool for the job
Existing testing methodologies are insufficient for Spring Boot upgrades:
- Unit tests exercise your code logic, not the framework’s serialization, autoconfiguration order, or dependency-injected behavior. A Jackson upgrade that changes
Optionalserialization won’t be caught unless you explicitly assert on that output. - Integration tests typically use synthetic or fixture data. They rarely cover the full shape of production requests—weird query params, optional headers, malformed-but-accepted inputs, or the exact JSON structures your clients send.
- Manual testing doesn’t scale and can’t cover the combinatorial explosion of API paths, headers, and payloads that production sees.
Traffic replay captures real production traffic—every API call, database query, and downstream service interaction. Replaying that traffic against your upgraded Spring Boot build gives you a pixel-perfect diff: exactly which responses changed, in which fields, and by how much. No speculation. No missed edge cases.
Speedscale makes this practical by:
- Recording all traffic—inbound and outbound—at the network level with the eBPF collector, including TLS-encrypted calls
- Mocking backend dependencies so you can replay traffic without a full staging environment
- Automatically diffing responses between the baseline and the upgraded build
- Integrating into CI/CD so you can gate Spring Boot upgrades on passing replay tests
Step-by-step: Validating your Spring Boot upgrade with Speedscale
Prerequisites
- Kubernetes cluster with your Spring Boot application deployed on the current version
- Speedscale operator installed
speedctlCLI installed locally- Application deployed as Kubernetes Deployment, StatefulSet, or DaemonSet
Step 1: Instrument your current deployment for recording
Install the Speedscale operator and add your Spring Boot service. The eBPF collector captures traffic at the kernel level without sidecars—no changes to your pods. It records inbound HTTP/gRPC, outbound database and API calls, and TLS traffic automatically.
Let the instrumented deployment run under representative production traffic:
- API services: A few hours during peak traffic
- Batch processors: At least one full batch cycle
- Event-driven services: A period covering all event varieties
Step 2: Create a snapshot
From the Speedscale UI or CLI, create a snapshot from the captured traffic:
speedctl create snapshot my-spring-service \
--start "2025-02-10T00:00:00Z" \
--end "2025-02-13T00:00:00Z" \
--name "spring-boot-baseline"
Review the snapshot to ensure it includes the API patterns, payload shapes, and edge cases you care about.
Step 3: Build and deploy your upgraded Spring Boot image
Upgrade your Spring Boot version in pom.xml or build.gradle, resolve any compile-time breaks (e.g., javax → jakarta), and build a new image. Deploy it to a staging or test namespace.
Step 4: Replay the snapshot against the upgraded deployment
Replay the baseline snapshot against your upgraded Spring Boot deployment:
speedctl replay my-spring-service \
--snapshot "spring-boot-baseline" \
--namespace staging \
--test-config standard
Speedscale replays inbound traffic to your app and mocks outbound dependencies with recorded responses.
Step 5: Analyze the replay report
In the Speedscale UI, inspect:
Response correctness: Compare response bodies field-by-field. Look for:
| Symptom | Likely cause |
|---|---|
| Field name changes | Jackson property naming, config change |
Optional / null handling differences | Jackson module or databind version |
| Date/numeric format changes | Serialization defaults |
| Missing or extra fields | Autoconfiguration, @JsonInclude |
| 500s on previously working paths | Removed API, autoconfig conflict |
Latency: Compare p50/p95/p99. Framework upgrades can change startup cost, connection pooling, or async behavior.
Error rate: Filter by status code. Requests that succeeded on the old version may fail on the new one.
Step 6: Fix, rebuild, replay—iterate until clean
For each regression:
- Diagnose: Use the RRPair diff to see the exact expected vs actual response
- Fix: Adjust configuration (e.g., register
Jdk8Module), update code, or align property names - Rebuild: Push a new image
- Replay: Run the same snapshot again
Repeat until the replay report is clean.
Step 7: Gate your CI/CD pipeline
Once validated, add replay tests to your CI/CD so future Spring Boot upgrades are continuously checked against the same traffic baseline. See the Speedscale CI/CD integration guide for GitHub Actions, GitLab CI, Jenkins, and more.
Using proxymock for local validation
You can validate the upgrade locally before deploying to a cluster using proxymock. For a deeper comparison of local replay tools, see Mitmproxy vs Proxymock. See the proxymock getting started guide for setup.
Note: The instructions below handle inbound traffic only. If your service makes outbound calls to databases, APIs, or other services, you may need custom CLI parameters or environment variables to reliably capture that traffic. See the proxymock docs for proxy configuration and outbound capture.
Record locally with current Spring Boot version
brew install speedscale/tap/proxymock
proxymock record --app-port 8080
# In another terminal, run your app with current Spring Boot version
java -jar my-spring-service.jar
# Send test traffic
curl http://localhost:4143/api/health
curl http://localhost:4143/api/users
curl http://localhost:4143/api/orders
# Stop recording (Ctrl+C)
Replay against upgraded Spring Boot
proxymock mock --in-dir ./proxymock/recorded
# In another terminal, run your app with upgraded Spring Boot
java -jar my-spring-service.jar
proxymock replay --in-dir ./proxymock/recorded --test-against http://localhost:8080
proxymock compare ./proxymock/recorded ./proxymock/replayed
The compare output shows exactly where the upgraded build differs from the baseline—same workflow as Speedscale, but entirely local.
Upgrade checklist
Before considering the Spring Boot upgrade complete:
- All API endpoints produce identical response bodies, or intentional differences are documented
- JSON field names, formats, and null handling match the baseline
- Latency p99 is within acceptable variance
- No new 4xx/5xx errors on previously successful paths
- Database queries and cache behavior are unchanged
- Actuator and health endpoints behave as expected
- Autoconfiguration and transitive dependencies are verified
- Replay tests are integrated into CI/CD as a regression gate
Conclusion
Spring Boot upgrades are necessary to stay current with security patches and features, but they introduce risk at every layer—Jackson, autoconfiguration, transitive dependencies, and removed APIs. Unit and integration tests often pass while production breaks because the bytes on the wire have changed.
Traffic replay turns upgrade validation into a deterministic process: record production traffic on the current version, replay it against the upgraded build, and diff the results. Every regression surfaces before users or clients are impacted. And once the upgrade is complete, the same pattern protects you from regressions in every future release. If you’re tempted to build your own replay harness, read about the scaling pitfalls first.