skip to content

Spring Cloud Contract

The JVM-native alternative to Pact: contracts in a Groovy or YAML DSL on the producer, tests generated from them, and stub JARs consumed through Stub Runner. Spring shops ask you to contrast the two.

on this pageshow

explore

questions

4

In Spring Cloud Contract, what does the producer's build generate from a contract file, and when does it fail?

level: middleimportance: must knowfreq 66%

answer

  1. The producer's repository holds the file
  2. The build generates the tests, not you
  3. One generated test method per contract
  4. A nominated base class does the setup
  5. Red when the real response diverges

basics

~20 s

Spring Cloud Contract's build plugin reads Groovy or YAML contract files on the producer, generates one JUnit test per contract that exercises the real controller, and fails the producer's build when the service's actual response does not match the contract.

solid answer

~50 s

Contracts are hand-written source files that live **in the producer's own repository**, either in a Groovy DSL built on `org.springframework.cloud.contract.spec.Contract` or in the equivalent YAML. At build time the Spring Cloud Contract plugin scans the configured `contractsDirectory` and emits **one generated test method per contract** into generated test sources. Each generated test extends a base class you nominate (`baseClassForTests`, or `packageWithBaseClasses` to map contract packages to base classes), and that base class stands the application up — MockMvc, `WebTestClient`, or a really-listening port — and seeds the fixture data the contract assumes. The generated test then sends exactly the declared request and asserts the declared status, headers and body fields. The producer's build goes red when the running service answers differently: changed status, renamed field, changed type, moved path. You never edit those tests; they are regenerated every build.

code

groovy · 20 lines
groovy
import org.springframework.cloud.contract.spec.Contract

Contract.make {
    description "returns the OCR status of a digitised scan"
    request {
        method 'GET'
        url '/scans/8417/ocr-status'
    }
    response {
        status 200
        headers {
            contentType applicationJson()
        }
        body(
            scanId: 8417,
            state: 'COMPLETED',
            confidence: 0.9731
        )
    }
}

go deeper

for a junior

Recall that with Spring Cloud Contract the contract file sits in the producer's repository and the build generates provider tests from it. You are not expected to configure the plugin or write the base class yet.

for a middle

Be ready to walk the generation step end to end: the scanned contracts directory, one generated test per contract, the base class that boots the app and seeds data, and exactly which producer changes turn the build red.

for a senior

Expect to diagnose a red generated test that is really a missing fixture, and to explain how you keep dozens of generated tests fast — base-class layering, test mode choice, and what you do when a contract must legitimately change.

for a principal

Own the argument for making the producer's own build the gate: who reviews contract files, how they are versioned with the service, and what a producer-authored contract does and does not prove about real callers across an estate.

## Producer-first: the contract is a source file Spring Cloud Contract inverts where the contract lives. In a consumer-driven tool the contract is a by-product of running the consumer's tests; here it is a **hand-written source file in the producer's own repository**, reviewed in the producer's pull requests and versioned with the endpoint it describes. Two dialects exist and they are equivalent: a Groovy DSL built on `org.springframework.cloud.contract.spec.Contract`, and a YAML form with the same request/response structure. A build plugin — `spring-cloud-contract-maven-plugin`, or the Gradle plugin — scans the directory configured as `contractsDirectory` and treats every file under it as generator input. That ownership is the first thing an interviewer probes, because everything else follows from it. Nobody *records* the contract. No consumer test run produces it. The producer can even write one for a consumer that has not been built yet. ## What the build generates For every contract file the plugin emits **one generated test method** into generated test sources, on every build. You do not commit or edit them; they are output, regenerated from scratch each time. A generated method does three things in order: 1. **Builds the exact request** the contract's `request` block declares — method, URL, headers, body. 2. **Sends it at the real application**, in whichever mode the build is configured for: MockMvc against the actual controller, `WebTestClient` for a reactive stack, or an explicit mode that hits a really-listening port. 3. **Asserts the response** the `response` block declares — status, headers, and every declared body field. Because the generated test needs an application to talk to and data to talk about, it **extends a base class that you nominate and hand-write**. `baseClassForTests` points every generated test at a single class; `packageWithBaseClasses` maps contract sub-packages to base classes by convention, so different endpoint families get different setup. The base class boots the context (or assembles a standalone MockMvc with the controller plus a stubbed service layer) and puts the fixture in place — the record that must exist for `/scans/8417/ocr-status` to answer `COMPLETED`. Nothing in the contract file references your domain types; the whole coupling to the application runs through that base class, which is why a contract file stays readable to someone who does not know the codebase. ## What turns the producer's build red The generated tests run in the producer's normal test phase, so a contract break is an ordinary build failure — no registry call, no other team's CI involved. | Change on the producer | Verdict | Why | |---|---|---| | Response field renamed | **red** | the generated assertion still names the old field | | Status changed 200 to 202 | **red** | the contract declares the status literally | | Field type changed number to string | **red** | the assertion checks the JSON type it was given | | Endpoint removed or path changed | **red** | the request the generated test sends now 404s | | A new, undeclared field added | green | assertions cover declared fields; extras are not forbidden | | Fixture missing in the base class | **red**, but a setup fault | it reads as a contract break; it is a data break | That last row is the practical trap. A failing generated test says the response did not match — it does not say why, and in real life a large share of failures are the base class no longer seeding what the contract assumes. ## A worked case An archive-digitisation workflow team owns `scan-service` inside a 23-service estate and keeps 41 contract files under its contracts directory. A refactor renames `confidence` to `ocrConfidence` in the OCR-status payload. No consumer is touched, no registry is consulted, and yet the producer's own build fails on the 3 generated tests that declare `confidence`, about 3.4 minutes into the same run that compiled the change. The engineer either restores the field name or edits the contract — and editing the contract is a visible, reviewable diff that says *this is a breaking change* in the pull request. That visibility is the point of keeping the file beside the code. ## Why this shape at all - The break lands on the team that caused it, in their own build, before merge. - The contract is a design document a reviewer actually reads, not a generated JSON dump nobody opens. - A producer can specify and freeze an endpoint before any consumer exists to record expectations. - The cost is honest and worth stating in an interview: a producer-written contract records what the producer **believes** its callers need. It is only as true as the review that approved it, and it does not by itself prove any caller depends on that shape.

  • The contract file never mentions your domain classes, so where does the data a generated test needs come from?
    From the base class you nominate with `baseClassForTests` or `packageWithBaseClasses`. It is the only hand-written half: it boots the application context or a standalone MockMvc setup and seeds the fixture the contract assumes. Contracts stay free of application types, which is why a base-class gap shows up as a puzzling contract failure.
  • If the producer adds an extra field to a response body already covered by a contract, does the generated test go red?
    No. The generated assertions check the fields the `response` block declares; an additional field is simply not asserted about. That makes additive change cheap, and it also means a contract only pins what it names — if a field matters to callers, declare it.
  • Does writing the contract in YAML instead of the Groovy DSL change what the build produces?
    No. Both dialects feed the same generator, so you get the same generated provider test and the same stub mappings. Groovy buys you programmable helpers and dynamic values; YAML is easier for non-JVM reviewers and for generating contracts from another source. Teams usually pick one and keep it consistent.

The contract file is a specification that compiles into its own exam: the producer writes the spec, and the build hands the producer the paper it must pass.

saying these in an interview costs you the question

  • Says the consumer writes the Spring Cloud Contract file
  • Thinks generated provider tests are committed and hand-edited
  • Cannot say what the nominated base class is for
  • Believes the generated test mocks the controller instead of calling it
  • Assumes an added response field fails the generated test
open as a page

How does Spring Cloud Contract's producer-first workflow differ from Pact's consumer-driven one in who must act?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Spring Cloud Contract puts the contract in the producer's repository, so the producer's own build fails first. Pact records the contract from the consumer's tests, so the provider's verification build fails and the provider must chase the consumer.

open as a page

What does a Spring Cloud Contract stub JAR contain, and how does Stub Runner use it?

level: middleimportance: should knowfreq 54%

basics

~20 s

A Spring Cloud Contract stub JAR is an ordinary Maven artifact published with the stubs classifier, holding WireMock JSON mappings generated from the producer's contract files. Stub Runner resolves it by coordinates and serves those mappings on a local port.

open as a page

In a Spring Cloud Contract DSL file, which parts drive the generated test and which drive the stub?

level: seniorimportance: nice to knowfreq 29%

basics

~20 s

One Spring Cloud Contract file compiles into two artefacts. Values wrapped in consumer(...) go into the WireMock stub; values wrapped in producer(...) go into the generated provider test. That lets the stub match loosely while the test asserts precisely.

open as a page