Skip to main content

Your Service Passes All Tests But Breaks Production: Detecting Inter-Service API Regression

· 8 min read
Evgenii Frolikov
Senior Java Architect | Expert in High-Load Systems & JVM Internals

Detecting inter-service API regression: before/after comparison of HTTP exchanges between microservices

TL;DR: The most dangerous bugs in microservices are not inside a service. They are between services. A code change can make a service pass all its local tests while silently altering what it sends to downstream APIs: different payload, missing header, changed error format. These regressions are invisible to unit tests, hard to catch with contract tests, and expensive in production. BitDive detects them by capturing real HTTP exchanges in execution traces and comparing them before and after a code change.

The Bug That All Tests Miss

Here is a scenario every microservices team has experienced at least once:

  1. A developer updates a shared library or changes a DTO mapping.
  2. All unit tests pass. The integration test suite is green.
  3. The service deploys to production.
  4. Hours later: a downstream service starts throwing deserialization errors, or silently processing wrong data.

The root cause: the service changed how it interacts with other services at the HTTP level. The actual bytes it sends over the wire are different. But no test was checking that.

Common triggers:

  • Jackson or ObjectMapper update changed date/enum/null serialization
  • DTO field renamed or removed
  • Include.NON_NULL toggled on or off
  • WRITE_DATES_AS_TIMESTAMPS enabled
  • MapStruct/ModelMapper update swapped field mapping
  • Spring Boot version bump changed default serialization
  • Feign interceptor refactoring dropped an auth header

None of these touch business logic. All of them change the runtime API contract between services.

Why Existing Tests Are Blind Here

Each testing approach has a structural blind spot when it comes to inter-service API compatibility:

Unit tests mock the HTTP client entirely. They verify local logic but never see the actual serialized request.

Contract tests (Pact) verify predefined examples. If an example does not cover the specific field, serialization format, or header, the regression slips through. A Pact contract that checks status exists does not catch "ACTIVE" becoming 0.

OpenAPI validation checks schema compliance but not runtime serialization behavior. The schema says string, but Jackson now serializes the LocalDate as a Unix timestamp.

End-to-end tests can catch the problem, but they are slow, flaky, and cover only a narrow set of scenarios.

The gap: no standard testing approach compares the actual serialized HTTP exchange before and after a code change.

What BitDive Sees That Other Tests Cannot

BitDive captures execution traces from running Java applications. Each trace includes every outgoing HTTP call with full details:

LayerWhat BitDive captures
EndpointHTTP method, URL, path variables, query parameters
Request headersAuthorization, Content-Type, X-Correlation-Id, tenant/feature headers
Request bodyThe actual serialized JSON as sent over the wire
Response statusHTTP status code
Response bodyThe actual response payload as received
Error detailsException class, message, stack trace

The key distinction: this is the real runtime exchange, not the Java object before serialization. If Jackson serializes a BigDecimal as "19.99" in the baseline but as 19.99 after an ObjectMapper change, the difference is visible in the trace.

Four Regressions That Traces Catch

1. Serialization Change After Jackson Upgrade

Before the change:

{
"createdDate": "2026-02-28",
"status": "ACTIVE",
"amount": "19.99"
}

After Jackson config update:

{
"createdDate": 1740700800000,
"status": 0,
"amount": 19.99
}

Unit tests pass. Contract tests pass (they only check field presence). BitDive's before/after trace comparison flags three diffs: date format, enum serialization, number type.

2. Missing Auth Header After Interceptor Refactoring

Before: Every outgoing request includes Authorization: Bearer ... and X-Correlation-Id: abc-123.

After: The interceptor was refactored. X-Correlation-Id is still sent, but Authorization is missing on one specific call path to the payment service.

No test covers this header on this specific route. The payment service returns 401. BitDive shows the header diff immediately in the trace comparison.

3. Changed Error Contract

Before: Downstream returns 404 with:

{"code": "CUSTOMER_NOT_FOUND", "message": "Customer 42 not found"}

After: Downstream returns 500 with plain text: Internal Server Error

The upstream service's error handler expects a JSON body with a code field. It now throws an HttpMessageNotReadableException. BitDive detects both the status code change and the response body format change.

4. Silent Call Sequence Change

Before: The service calls GET /accounts/42, then POST /transactions with account data from the response.

After refactoring: The service calls POST /transactions directly, without fetching account data first. It sends a hardcoded default instead of the real account segment.

Contract tests for each endpoint pass individually. The business scenario is broken. BitDive detects that one call disappeared and the remaining call sends different payload data.

How BitDive Detects These Regressions

BitDive uses before/after trace comparison to catch API regressions:

  1. Capture a baseline trace. Trigger the business scenario via a real API call (curl, Postman, your frontend). BitDive's agent captures a trace with all outgoing HTTP exchanges.
  2. Make a code change (refactor, library upgrade, DTO update, Jackson config change).
  3. Trigger the same scenario again. Call the same endpoint on the updated service. BitDive captures a new trace.
  4. Compare the two traces across every layer: endpoint diff, header diff, request body diff, response diff, status diff, call sequence diff.

Because traces are captured from real API calls, the comparison operates on actual serialized HTTP exchanges, not on mocked data or theoretical schemas. If the code now sends a different payload, drops a header, or changes the call sequence, the diff shows exactly what changed.

Not Every Diff Is a Bug

A useful system must distinguish noise from breakage. BitDive classifies differences:

Expected (not a regression):

  • New optional field in the response
  • New trace or correlation header
  • Different timestamps, UUIDs, request IDs (automatically excluded)

Critical (likely a regression):

  • Required field disappeared from request body
  • Field type changed (string to number, date format changed)
  • Auth header missing
  • Error status code changed (404 became 500)
  • Call removed or new unexpected call appeared
  • Response body structure changed

Fields like requestId, timestamp, traceId, and UUID are automatically masked. Custom masking and comparison policies can be configured in the Configuration Guide.

Where This Matters Most

Inter-service API regression control delivers the most value in systems with:

  • Many internal REST APIs between microservices
  • Frequent library and framework upgrades
  • Strong reliance on DTOs and internal contracts
  • Active refactoring of integration layers
  • AI-assisted development (where agents may change serialization behavior without understanding downstream impact)

In these environments, the most expensive production bugs are not "the code does not compile." They are "the services no longer interact the same way."

Detect API Regressions from Real Traces

BitDive captures real HTTP exchanges between services and compares them before and after code changes. Serialization drift, missing headers, altered error responses: caught automatically in your existing JUnit tests.

Try BitDive Free

FAQ

How is this different from Pact contract testing?

Pact verifies that a service conforms to predefined contract examples. BitDive verifies that actual runtime API behavior remained the same after a code change. They complement each other. Pact catches explicit contract violations. BitDive catches implicit changes that fall outside the contract scope: serialization drift, header changes, error body mutations. See the full comparison.

Do I need to write separate tests for API regression?

No. BitDive compares traces captured from real API calls. Trigger the same business scenario before and after a code change, and BitDive diffs the outgoing HTTP exchanges automatically. No test code needed for this verification.

What about performance overhead?

BitDive captures traces using a standard Java Agent with 0.5-5% overhead. Trace comparison happens at test time, not at runtime, so there is no production performance impact beyond the capture phase.


Try BitDive Free