Module 4: Verifying The Contract From Both Sides
7. Who owns the contract: consumer-driven
Description
Lessons 5 and 6 left a question unanswered, and it's the most important of all. When the provider does one thing and the consumer expects another —when get stops raising, or when find_by_room doesn't come ordered—, who's right? Who defines what's promised? It's not a philosophical question: it's the one that decides, in each conflict, whether the red points to a provider bug or a consumer over-assumption. And the answer has its own name in the industry: the contract is consumer-driven, "driven by the consumer". The contract's owner is the one who consumes it, not the one who implements it.
This can sound backwards. Intuition says that the one who builds the repository —the provider— should decide how it behaves. But think of it from the component's reason for being: SqliteBookingRepository doesn't exist for itself; it exists so that BookingService uses it. A method no one calls doesn't need to exist; a behavior no consumer needs doesn't need to be promised. That's why the consumer's needs are the contract's clauses: the consumer needs save-and-read, so that's a clause; it needs get to raise so it can distinguish the missing id, so that's another. The provider doesn't invent what to promise; it promises to fulfill what the consumer needs. This lesson makes that property explicit —the consumer owns the contract— and shows how it's automated between services in the concept of Pact, the tool that took this idea to the network without you having to install it to understand it.
Connection to the module: this is the governance lesson, the one that frames everything before. Lessons 2 and 3 gave you the two sides; lesson 4, the guarantee of running them together; lessons 5 and 6, the two ways to break the agreement. This one answers who rules the agreement, and with that closes the module's conceptual arc. Lesson 8 puts it into practice in the mini-project. Afterward, module 5 abandons the isolated contract to test the real components together —real integration—.
Analogy: the client who orders the suit
Imagine a tailor and a client. The client is going to order a suit for a wedding: they need it to fit them, with their measurements, for that occasion. Who defines the suit's specifications —the sleeve length, the back width, the color? The client, because the suit exists to serve them. The tailor doesn't decide "I'm going to make the sleeves this length because I like it"; they take the client's measurements and promise to fulfill them. The client drives the specification; the tailor executes it. It's customer-driven, so to speak.
Now, something subtle but decisive: the specification is defined from what the client really needs, not from everything the tailor could do. If the tailor adds, on their own, a secret pocket the client never asked for, that pocket isn't part of the agreement —and if one day they remove it, the client can't complain, because they never ordered it—. Conversely: if the client takes for granted that the suit comes with a matching handkerchief, but never specified it, and the suit arrives without a handkerchief, the blame is the client's for over-assuming, not the tailor's. The specification is exactly what the client asked for: neither the tailor's unrequested gifts, nor the client's tacit assumptions.
BookingService is the client; SqliteBookingRepository is the tailor; the contract is the suit's specification. The clauses are the measurements the consumer needs —save-and-read, get that raises, filter by room—. A behavior the provider has but no consumer asked for (insertion order, object identity) is the secret pocket: it's not part of the agreement, and relying on it is over-assuming (lesson 6). A behavior the consumer needs but the contract doesn't verify is the taken-for-granted handkerchief: it has to be written into the specification or stop being expected. The specification's owner is the client, because the suit —and the repository— exist to serve them.
Worked example: the consumer defines, the provider verifies
"Consumer-driven" isn't just an idea; it has a concrete shape in how you write and run the tests. The flow has two halves you already built separately, and seeing them together reveals the property. First half: the consumer defines its expectations. The consumer test (lesson 2) is where BookingService declares what it needs from the repository: it needs save-and-read to work, and it needs get to raise for a missing id. Those expectations are the contract in embryo. Second half: the provider verifies them. The provider test (lesson 3) takes those same expectations —turned into clauses— and checks that the real SqliteBookingRepository fulfills them.
Let's run the two halves together: the consumer's expectations on top, the provider verification at the bottom. It's the complete consumer-driven flow, in-process.
What to expect. On my machine (Python 3.14.0, pytest 9.1.1):
python3 -m pytest tests/test_consumer_bookingservice.py tests/test_repository_contract.py -v -k "consumer or sqlite"
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
collected 10 items / 4 deselected / 6 selected
tests/test_consumer_bookingservice.py::test_book_persists_a_retrievable_booking PASSED [ 16%]
tests/test_consumer_bookingservice.py::test_cancel_of_a_missing_booking_propagates_the_contract_error PASSED [ 33%]
tests/test_repository_contract.py::test_save_then_get_returns_the_same_booking[sqlite] PASSED [ 50%]
tests/test_repository_contract.py::test_get_of_a_missing_id_raises[sqlite] PASSED [ 66%]
tests/test_repository_contract.py::test_saving_the_same_id_twice_updates_not_duplicates[sqlite] PASSED [ 83%]
tests/test_repository_contract.py::test_find_by_room_returns_only_that_rooms_bookings[sqlite] PASSED [100%]
======================= 6 passed, 4 deselected in 0.01s ========================
Read it top to bottom as the consumer-driven flow. The first two lines are the consumer's expectations: BookingService declares, in green, "I need to save-and-retrieve" and "I need cancel of a missing id to propagate the contract's error" —that is, "I need get to raise"—. The four at the bottom are the provider verification: the real SqliteBookingRepository fulfills each clause born from those needs. Notice the direct link: the consumer's expectation test_cancel_of_a_missing_booking_propagates_the_contract_error (top) and the provider's clause test_get_of_a_missing_id_raises[sqlite] (bottom) are the same promise seen from its two owners: the consumer asks for it, the provider fulfills it. The contract didn't fall from the sky or get dictated by the provider: it sprang from what the consumer needs, and the provider verified itself against it. That's consumer-driven made into pytest output.
Why the owner is the consumer and not the provider
Let's pause on the argument, because inverting the intuition correctly is the heart of the lesson. There are three reasons the contract is owned by the consumer.
Because the component exists for the consumer. A provider is a means, not an end: SqliteBookingRepository exists so that BookingService persists bookings. Its promises are justified by the needs they serve. If tomorrow no consumer needed find_by_room, that promise could disappear without anyone missing it. Promises are born of demand, and the demand is set by the consumer.
Because the consumer is the one who suffers if they break. When get stops raising, the one who breaks is cancel —the consumer—. The provider "works" (it returns None without an error); it's the consumer who pays the price. Whoever suffers the non-compliance has the natural authority to define what constitutes compliance. The tailor doesn't suffer if the sleeve is short; the client does, and that's why the client defines the length.
Because it avoids over-promising. If the provider defined the contract, the temptation would be to promise everything it does —insertion order, object identity, every incidental detail—, and each of those details would become a promise that ties every future implementation. By letting the consumer define, the contract contains only what someone really needs, and stays minimal. A minimal contract leaves the provider free to change everything else without breaking anyone. Consumer-driven isn't just "who rules"; it's the discipline that keeps the contract small and, therefore, the provider flexible.
From here comes the resolution of the conflicts in lessons 5 and 6. If the consumer needs something and the contract promises it, the provider must fulfill it (breaking it is a breaking change: lesson 5). If the consumer assumes something the contract does not promise, the consumer is wrong (over-assumption: lesson 6). The contract —defined by what the consumer wrote it needs, not by what it tacitly took for granted— is the referee. And that's why writing the consumer's expectations well matters so much: they're the law.
The concept of Pact: consumer-driven between services, over the network
Everything you did by hand has an industrial incarnation for when the two sides aren't in the same process, but in separate services that talk over HTTP: the consumer is a service, the provider is another, they run on different machines, different teams maintain them. There you can't simply import the other's fake and run a shared battery. Pact is the tool that takes the consumer-driven idea to that world. It's worth understanding its shape, because it's exactly this module's pattern, stretched by the network.
Pact's flow, conceptually:
-
The consumer generates the contract. In its suite, the consumer runs its tests against a mock provider Pact spins up —a stand-in that responds according to what the consumer expects, just as the fake was our stand-in in lesson 2—. When it runs, Pact records each expected interaction ("when I ask for
GET /bookings/xyzof a missing id, I expect a404") and writes them to a pact file: a JSON with the consumer's expectations. The contract is produced by the consumer, from its own tests. That's consumer-driven literally: the contract artifact is born on the consuming side. -
A broker shares the contract. The pact file is published in a Pact Broker, a central service where the contracts live. The broker is the meeting point between teams that don't share code: the consumer publishes its expectations there, the provider picks them up from there.
-
The provider verifies itself against the contract. The provider's team takes the pact file from the broker and runs the provider verification: it reproduces each recorded interaction against its real implementation and checks that it responds as the consumer expects. It's exactly the lesson 3 provider test —"given X, I return Y"—, only the X and the Y come from the pact file the consumer generated, not from a battery written by hand in the same repo.
-
A gate before the deploy. The broker can answer the question "can I deploy this version without breaking anyone?" —the Can I Deploy function— by cross-referencing which consumers depend on which verified versions of the provider. It's the networked version of "run the contract before merging" from lesson 5: the breaking change is caught between services before the deploy, not just within a process.
The correspondence with what you did is one-to-one. Our FakeBookingRepository is Pact's mock provider. Our shared battery is the pact file. Running the battery against the SqliteBookingRepository is the provider verification. And running the contract before merging is the Can I Deploy. The difference is only the medium: we're in a process, with an import; Pact is between services, with a JSON and a broker. That's why this guide teaches the idea in depth and doesn't install the tool: once you understand the two sides, the shared battery, and the verification, Pact is that same skeleton dressed for the network. When you see it in a real system, you'll recognize every bone.
When this is worth it (and when not)
Consumer-driven and Pact solve a specific problem: two components that evolve separately and must keep talking to each other. The value grows with the distance between the sides. It's very worth it when the consumer and the provider are different services, in different repos, maintained by different teams that deploy at different times —there a breaking change is invisible until it blows up in integration, and the contract is the only net—. It's worth it, in its hand-built version (this module's), when you have a seam with several implementations (the fake and the real one) that must match. And it's worth less when the two sides live in the same module, change together in the same commit, and are tested together: there a direct integration test may be enough, and setting up the contract apparatus would be over-engineering.
The rule: contract testing pays off when the cost of an undetected breaking change is high and its natural detection is late (separate services, independent deployments). If your two sides always change and are tested together, the contract adds less than a direct integration —which is exactly what module 5 starts to build—.
Common mistakes
Letting the provider define the contract "because it's the one that knows how it works". What happens: the provider's team writes the contract from everything its implementation does, and the consumer adapts to that. Why it happens: the provider seems the authority on its own behavior. How to detect it: if the contract includes promises no consumer uses (insertion order, internal details), it's driven by the provider, not by the consumer. How to fix it: invert the direction. Have the contract be born from what the consumers need —from their expectation tests—, not from what the provider does. That way it stays minimal and doesn't tie the provider with incidental promises it will one day want to change.
Confusing the pact file (the artifact) with the concept. What happens: someone believes that "doing contract testing" is necessarily generating a JSON with Pact and setting up a broker. Why it happens: the tool is taken for the idea. How to detect it: if you think there's no contract testing without Pact installed, you confuse the vehicle with the journey. How to fix it: the concept is "the consumer defines expectations, the provider verifies them, and both are tested against the same agreement". This module's parametrized battery is contract testing, without a single JSON. Pact adds the pact file and the broker when the sides are separated by the network; in-process, they're not needed. Understand the idea first; the tool is optional and depends on the context.
Treating the contract as untouchable instead of as a living agreement. What happens: when the consumer needs something new, someone adds it as a tacit assumption instead of changing the contract, "so as not to touch the battery". Why it happens: modifying the contract feels heavy. How to detect it: if the consumer depends on behaviors that aren't in any clause, the contract stopped reflecting what the consumer really needs —it's outdated—. How to fix it: the contract is consumer-driven and therefore living: when the consumer's needs change, the contract is changed (a new clause, verified against both sides) alongside. A contract that isn't updated with the consumer's needs stops being the reliable referee of lessons 5 and 6, and over-assumptions slip through the gap between what the consumer needs and what the contract says.
Exercises
Exercise 1 — Trace the link. In the worked example's output, the consumer line test_cancel_of_a_missing_booking_propagates_the_contract_error and the provider line test_get_of_a_missing_id_raises[sqlite] are connected. Explain how they correspond in the consumer-driven flow, and who "owns" that concrete promise.
See solution
The two lines are the same promise seen from its two task owners:
- The consumer's (
test_cancel_..._propagates_the_contract_error) is whereBookingServicedeclares its need: "I, when I cancel an id that doesn't exist, need the error to propagate; that is, I needgetto raise". That's the consumer's expectation, the one that defines the clause. - The provider's (
test_get_of_a_missing_id_raises[sqlite]) is where theSqliteBookingRepositoryverifies that it fulfills that need: "given a missing id, I raiseKeyError".
Who owns the promise: the consumer. The promise "get raises for a missing id" exists because cancel needs it to work; it's not a whim of the repository. If cancel didn't exist and no other consumer needed to distinguish the missing id by an exception, this clause would have no reason to be in the contract. The consumer defined it with its need; the provider only promises to fulfill it. That's why, when the provider broke it (lesson 5), the red was legitimate: the provider failed a promise the consumer had the right to demand. The direction of ownership —from consumer to provider— is what makes that red mean "the provider failed" and not "the consumer is demanding".
Exercise 2 — From the fake to the pact file. Establish the correspondence between our hand-built version and Pact's flow, for each piece: (a) the FakeBookingRepository; (b) the shared contract battery; (c) running the battery against SqliteBookingRepository; (d) running the contract before merging a provider change.
See solution
- (a) The
FakeBookingRepository↔ Pact's mock provider. Both are the stand-in the consumer is tested against: a double that responds according to the contract, without the real implementation. In-process it's an object you import; in Pact it's a fake server the tool spins up and against which the consumer sends its real requests. - (b) The shared contract battery ↔ the pact file. Both are the artifact that captures the agreement: the clauses both sides must fulfill. In-process it's a parametrized test file; in Pact it's a JSON with the expected interactions, generated by the consumer.
- (c) Running the battery against
SqliteBookingRepository↔ the provider verification. Both are "the real provider answers for itself against the agreement": take the expectations and check that the implementation fulfills them. In-process it's-k sqlite; in Pact it's the verification task that reproduces the pact file against the provider. - (d) Running the contract before merging ↔ the broker's Can I Deploy. Both are the gate that prevents a breaking change from reaching production: verify the compatibility before the deploy. In-process it's running the battery on your machine or CI; in Pact it's asking the broker whether the version you want to deploy is compatible with the verified consumers.
The pattern is identical in all four; only the medium changes (a process with imports versus services with JSON and broker). Understanding the left column is understanding the right one.
Exercise 3 — Consumer-driven or direct integration? For each scenario, decide whether a consumer-driven contract (Pact-style) is appropriate or a direct integration test is enough, and why: (a) the billing team (another service, another repo, another deployment) consumes your booking API over HTTP; (b) BookingService and SqliteBookingRepository live in the same Reservo module and are always deployed together.
See solution
- (a) External billing service → consumer-driven contract (Pact). The sides are separated by the network, in different repos and deployments, maintained by different teams. A breaking change in your API would be invisible to you until the billing service broke in integration —late and expensive—. Here the consumer-driven contract is the net that's needed: billing publishes its expectations as a pact file, you verify your API against them before deploying, and Can I Deploy prevents breaking them. The distance between the sides is exactly what justifies the apparatus.
- (b)
BookingServiceandSqliteBookingRepositoryin the same module → direct integration (probably). The two sides change together, in the same commit, and are tested together. The cost of a breaking change is low because it's detected immediately when running the module's suite, and setting up the pact file + broker apparatus would be over-engineering. An integration test that usesBookingServicewith the realSqliteBookingRepository(module 5) covers the seam without the between-services contract machinery. (That said, this module's hand-built contract battery is still useful here to keep the fake honest against the real one —that's not over-engineering, it's cheap—; what would be excessive is Pact's JSON and broker for two pieces that live together.)
The rule that decides: how much distance is there between the sides? A lot (separate services, repos, teams, deployments) → formal consumer-driven contract. Little (same module, same deployment) → direct integration, maybe with the hand-built contract battery for the fake, but without the complete apparatus.
Summary and next step
In this lesson you answered the module's governance question: the consumer owns the contract. The component exists to serve the consumer, the consumer is the one who suffers if the promises break, and letting it define them keeps the contract minimal and the provider flexible. With the client who orders the suit you understood that the specification is defined by whoever needs it —neither the tailor's unrequested gifts, nor the client's tacit assumptions—, and you saw the consumer-driven flow in real output: the consumer's expectations on top, the provider verification at the bottom, the same promise seen from its two owners. And you mapped all your hand-built work to the concept of Pact —the fake is the mock provider, the battery is the pact file, running against SQLite is the provider verification, running before merging is the Can I Deploy—, understanding that Pact is this same skeleton dressed for the network, and why this guide teaches the idea without installing the tool.
Before moving on you should be able to: argue why the contract's owner is the consumer and not the provider; use that property to resolve the conflicts in lessons 5 and 6 (broken promise versus over-assumption); map the four pieces of your hand-built version to Pact's flow; and decide when a consumer-driven contract pays off and when a direct integration is enough.
With this lesson you close the module's conceptual arc: the two sides, the shared battery, the two ways to break it, and who rules. Lesson 8 puts it all in your hands in the mini-project —verify the contract from both sides and catch a breaking change you introduce—. And afterward, module 5 takes the natural next step: leave the isolated repository contract and test the real components together, crossing the seam for real. The contract guaranteed the fake doesn't lie; integration verifies that the real pieces fit.
Resources
- docs.pact.io — Introduction and How Pact works — the official reference for consumer-driven contract testing: the mock provider, the pact file, the broker, and the provider verification; the complete map of this module's industrial version.
- docs.pact.io — Can I Deploy — the broker function that decides whether a version can be deployed without breaking its consumers; the networked version of "run the contract before merging" from lesson 5.
- Martin Fowler — Consumer-Driven Contracts — the article that named the pattern and explains why the consumer drives the contract; the conceptual basis of the whole lesson.
- pytest documentation — Selecting tests with
-k— the selector with which we ran the consumer's expectations and the provider verification together (-k "consumer or sqlite") to see the complete consumer-driven flow.