Implementing Pact Contract Testing in CI/CD for Microservices

Microservices most often break at the seams: one service is updated and neighboring ones fail. We implement turnkey contract testing in the release process: compatibility checks run automatically before release and filter out incompatible versions. The result is fewer integration failures and release cycles that are several times shorter.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1365
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1330
  • B2B Advance company logo design
    B2B Advance company logo design
    776
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1077
  • AIDER company logo development
    AIDER company logo development
    1022
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1113

Contract testing: where microservice integrations break

Microservices are released independently: each has a green set of its own tests, the build passes without issues, but at the integration environment the boundary falls apart.

One service expects a field to be mandatory, another returns it empty; the provider changed the response code to an error — the consumer treats it as success and writes corrupted data to its database.

Interface incompatibility almost never looks like an obvious failure. A renamed field, a changed date format, a new mandatory request parameter, a changed value type — individually each service remains functional, but the connection stops working.

Internal tests will not catch this: they check the logic inside the service and run on stubs written by the same team that writes the service itself.

The integration bottleneck shifts to release: the error is caught not by the one who introduced it, but by the on-call engineer at the environment or by support after deployment.

The cost is post-release regression, rollback, manual scenario runs, and extra coordination between teams instead of a single check at the entrance.

We explain how contract testing closes this gap: the agreement about boundaries is fixed as a verifiable artifact in the repository, both sides are run against it before the build, and the divergence surfaces at the moment of the change, not a week later at the service boundary.

Measurable effect: fewer failures at service boundaries

Compatibility checks move divergences from the production environment to the build stage.

While the contract between services lives in chat, incompatibility is discovered in operation: the tests inside each service are green, but at the boundary the field format or response code breaks.

Once checks are connected to the build pipeline, an incompatible version does not reach the environment — the build stops at the verification step, and the author of the change immediately sees which condition diverged from the consumer's expectation.

The effect is visible in the first weeks. The share of failures at boundaries drops to isolated cases — scenarios not covered by the contract remain, and they are closed with separate checks.

Release speed increases: format coordination stops being chat between teams, the contract lives in the repository and is checked automatically. Service downtime decreases because an integration failure stops being a release surprise.

The third effect is recovery time. An incident at the boundary used to mean either a release rollback or joint debugging by two teams while part of the functionality was unavailable.

With versioned contracts, incompatibility is confirmed before deployment, and the fix arrives as a single change from both sides — recovery takes one build cycle instead of a series of approvals.

We build checks on Pact: two-sided verification confirms that the provider fulfills consumer expectations, and the consumer is checked against real behavior.

A side effect is that the contract becomes documentation of the boundary, which a new person uses to onboard into the service.

Check options: contract testing and alternatives

Service boundaries can be checked in four ways, and the choice determines how early a defect is found and how much its fix costs. Below are real alternatives that teams choose between: from manual exchanges to formal contracts fixed as a separate versioned artifact.

The difference between them is the check point and the cost of the run. Manual checking and a shared environment validate the whole system, but late and expensively: the defect surfaces already at the acceptance stage.

Stubs provide speed but diverge from the real service behavior, while a contract checks the boundary before release and does not require bringing up the entire environment.

Option When it fits Limitations of the approach
Manual boundary checking One-off exchanges, 2-3 interactions, infrequent changes Does not scale, the result depends on the person, the error reaches prod
End-to-end tests on a shared environment 5-10 critical scenarios, the full end-to-end path Long run, the environment is occupied by one team, failure of any service nullifies the result
Service stubs The consumer is still in development, the external system is unavailable in the environment The stub describes expectations but drifts from the real response without verification
Formal contracts Many consumers of one service, frequent releases The check must be embedded in the build and ownership of the contract must be agreed

In practice, the options do not compete but divide areas of responsibility: contracts cover boundaries between internal services, end-to-end tests remain for critical paths, stubs for unavailable external systems. Then the limitations of each approach do not add up but overlap.

Implementation order: from boundary audit to handover to the team

Implementing contract checks is not installing a library but an order of work where sequence matters more than speed. If you enable checks on all boundaries at once, the team gets an avalanche of failures on every merge and disables them.

Therefore, the implementation stages go from inventorying connections to handover: each leaves an artifact that the next one relies on.

  1. Boundary audit. We assemble a call map: who calls whom, which fields the consumer actually reads, where expectations diverge from the actual response. The output is a list of connections prioritized by change frequency and criticality.
  2. Contract scheme. For each connection, we fix the consumer's expectations: mandatory fields, types, response codes, error behavior. Contracts on Pact are versioned and live in the provider service repository.
  3. Check setup. We connect the run to the build pipeline: the check runs on every commit and fails before artifact publication. Separately, we set up a dashboard showing exactly which boundary is broken.
  4. Environment verification. We run contracts on a copy of the production environment with a real data profile — this surfaces divergences invisible with stubs: date formats, empty values, different error handling.
  5. Divergence triage. We classify every failure: genuine incompatibility, false positive, or outdated expectation. False ones go into configuration, genuine ones into a fix plan for both sides of the boundary.
  6. Handover to the team. We hand over documentation on connections, rules for changing contracts, on-call regulations, and engineer training. After that, the team edits contracts itself, without our involvement.

Some divergences are found already during the audit — before the first test run.

Implementation artifacts: boundary schemes and versioning rules

The value of implementing contract testing is not the fact that checks are enabled but the set of artifacts that remains with the team. Without it, knowledge rests on two engineers and falls apart at the first rotation. Below is what we hand over and keep up to date.

  • Interface descriptions for each consumer: which fields the service returns, which are mandatory, what happens on an empty response, and which error codes the consumer can expect.
  • Boundary scheme — a "consumer → provider" map with owners, connection criticality, and an indication of whose release is blocked when a check fails.
  • Contract versioning rules: adding an optional field is a compatible change; narrowing a type or removing a field is breaking, requires a new version and notification of consumers.
  • Release regulations: the procedure for publishing a contract version, the mandatory verification window at the provider, and the rule by which the consumer's previous version continues to work.
  • Compatibility matrix — a "consumer version × provider version" table with explicitly marked allowed and forbidden combinations.
  • On-call instructions: what to look at in the check report, how to distinguish a broken contract from a failed environment, and how to roll back a contract.
  • Onboarding documentation: how to add a new service, where to get reference examples, and how to run verification locally.

The set survives a team change: a new engineer connects a service according to instructions, not a colleague's memory, and sees in advance which connections must not be broken.

Case study: how release time decreased after boundary checks

A network of 40+ services that six teams release at their own pace.

Boundary compatibility there was checked only on a shared environment, after all components were built at once: a change in one service broke a consumer a day or two later, when the cause had already dissolved among dozens of edits, and the culprit was sought in chat between teams.

Metrics before and after

Metric Before After
Change release time 9–12 days 2–3 days
Share of boundary failures in total incidents more than a third isolated cases
Integration incident triage 4–6 hours up to 40 minutes
Point when incompatibility is detected after 1–2 days on a shared environment at build, before deployment

What we did

Consumer expectations were moved into a contract next to the service code — a change to the public part is now visible in the same change as the logic.

A contract broker on Pact compares versions of both sides and does not allow deployment until at least one consumer confirms compatibility. The shared environment was offloaded: end-to-end scenarios remained, while checking each pair moved into the build.

The substantive effect was a change in the class of failures reaching prod: version incompatibility stopped being found by users.

Six months after implementation, the share of rollbacks due to boundary issues dropped almost to zero, and the freed-up time went to triaging functional defects that had previously been postponed.

What to do if services are already connected directly?

Direct service connections are already working and delivering results, so they cannot be broken all at once: while the system has hard calls, changing one participant brings down its neighbors.

Gradual implementation is not "enable checks for everyone at once" but one pair at a time — we start with the most loaded or most frequently changing interaction.

The first step is not written by hand. The consumer is run in recording mode: real requests and responses are turned into a contract — a file describing what the service actually expects from the provider.

With Pact, this is done without changes to business logic: only the fields and response codes already used in the code are recorded.

Then the check runs in observation mode: the contract is executed in the build pipeline but does not block release; it highlights divergences. The team sees the real picture of version compatibility and the amount of connection debt, while delivery does not stop for a day.

When there are no divergences for two or three iterations in a row, the check for this pair is switched to blocking mode — and the cycle is repeated on the next connection.

The transition plan becomes measurable: for each interaction, it is clear which step it is at — recording, observation, or blocking.

For already deployed services, a backward compatibility rule is fixed separately: as long as the provider has consumers of the previous version, the contract can only be extended — fields and new codes can be added, but existing ones cannot be deleted or have their meaning changed.

Outdated contracts are marked and retired when the last consumer has moved to the new version — this way version compatibility stops being a verbal agreement.

Engineer concerns: version compatibility and false positives

Concerns about implementation are almost always the same: checks will slow down the build, false positives will start blocking releases, and teams simply do not have interface descriptions. Below is how each of them is addressed at the configuration level, not with promises.

Will contract checks slow down the build pipeline?

The contract run goes as a separate job in parallel with unit tests and takes seconds, not minutes. The contract is published once when expectations change; after that, comparison uses the already saved artifact.

If build time is critical, we move the compatibility check to the step before deployment to the environment — then it slows down only the release, not every branch.

Will false positives stop releases?

A trigger means a divergence between the consumer's expectations and the actual response — this is a signal, not noise. False cases come from checks without contract versioning, so we fix a rule: comparison is performed only with compatible contract branches. Unstable checks go into quarantine with mandatory triage, not silently disabled.

We don't have interface descriptions — where do we start?

Descriptions do not need to be written by hand in advance. We capture the current behavior of services, fix it as a reference, and then change only where expectations diverge.

This way, implementation does not require stopping development and immediately provides a visible map of connections between services.

Who is responsible for checks after implementation?

Responsibility is divided: the consumer describes what it needs from its neighbor, the provider describes what it guarantees. The contract lives in a registry, and each has an owner from the team.

We hand over this regulation along with the configuration so that the checks do not remain "ownerless."

What about check support after handover?

The change regulations, instructions for triaging triggers, and on-call training remain. We support the first period of edits until teams start changing contracts independently without our engineers.

Let's discuss the task: what to send for an assessment and plan

To come back with an assessment and plan rather than general words, we need a minimum of inputs. The more accurate the source data, the fewer assumptions in the work plan and the more realistic the pilot timeline.

  • Service diagram: who communicates with whom, which calls are synchronous, which go through a message broker and in what format.
  • List of providers and consumers: which contracts are already described, and where manual agreements between teams live instead.
  • Current build pipeline: where checks run, how long the full run takes, which checks block branch merge.
  • Examples of integration failures: what broke, at what stage it was noticed, how much time triage and fixing took.
  • Repositories and access: read permissions are sufficient, plus a description of environments — test and pre-production.
  • Constraints: release regulations, deployment windows, information security and regulatory requirements.
  • What we consider the pilot result: which services we include first, which teams are in scope, by what deadline we expect the effect.

Then we assemble this into a work plan: the order of connecting services, check points in the pipeline, participant roles, and rollback mode if checks start blocking deployment.

Describe the task — send what you have: even a partial set of source data will allow us to come back with an assessment and pilot plan, and we will clarify what is missing during the review.