New: Debug encrypted microservice traffic with Speedscale's eBPF collector Read the announcement

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

IssueExampleRoot cause
Optional serialization failsInvalidDefinitionException at runtimeJackson Databind upgrade drops implicit JDK8 module
Field naming changecreatedAtcreated_atJackson property naming / config change
Numeric format change1250.01250Serialization default changes
Date format changeTimestamp vs ISO-8601 stringJackson 2→3 or config drift
Null handlingnull 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 Optional serialization 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:

  1. Recording all traffic—inbound and outbound—at the network level with the eBPF collector, including TLS-encrypted calls
  2. Mocking backend dependencies so you can replay traffic without a full staging environment
  3. Automatically diffing responses between the baseline and the upgraded build
  4. 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
  • speedctl CLI 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., javaxjakarta), 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:

SymptomLikely cause
Field name changesJackson property naming, config change
Optional / null handling differencesJackson module or databind version
Date/numeric format changesSerialization defaults
Missing or extra fieldsAutoconfiguration, @JsonInclude
500s on previously working pathsRemoved 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:

  1. Diagnose: Use the RRPair diff to see the exact expected vs actual response
  2. Fix: Adjust configuration (e.g., register Jdk8Module), update code, or align property names
  3. Rebuild: Push a new image
  4. 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.

Resources

Stop writing API mocks by hand

proxymock records real traffic from your running app and replays it as mocks — HTTP, gRPC, Postgres, Kafka, and more. Install in 30 seconds, no account required.