Skip to content
kc@kumarChandrachooda.com:~$ cd /blog/when-contract-testing-pays-for-itself && read --section="top" 0%
.NET

When Contract Testing Pays for Itself

The series retrospective - what a thirty-file library taught me that a full-featured one couldn't, Pactify's honest limit list versus PactNet, the broker maturity ladder, and the org-shaped question hiding under the tooling.

By Kumar Chandrachooda 26 Jul 2025 7 min read

Seven posts ago I claimed that reading a small library beats using a big one through its documentation. Series over, claim due for settlement. This retrospective is the honest ledger: what Pactify — Dariusz Pawlukiewicz's open-source “very simple contract testing tool for .NET Core,” which I've now read to the last line — gets right, the complete list of what it can't do, when you should reach for PactNet instead, and the question underneath all the tooling: when does contract testing actually pay for itself?

What the small library gets right

In-memory verification is the killer feature, and Pactify made me see it. CreateFor<Startup>() replaying a consumer's real expectations through your actual middleware pipeline, at unit-test speed, with zero deployed infrastructure — that's the moment contract testing stops being a diagram and starts being a build step. Every tool in this space does something equivalent; Pactify does it in five readable lines, which is why I now understand it instead of merely invoking it.

Structural contracts are the right default. The matching engine (part 6) is only trustworthy in IgnoreContractValues = true mode — and that turned out to be a feature disguised as a limitation. “Every property my consumer reads still exists, with the right status and content type” catches the field rename, the deleted property, the route change: the actual outages. Value-matching contracts, in my experience even with richer tools, mostly catch test-data drift.

The pact as dependency inventory. The quiet win nobody advertises: after a few months of pacts, the provider team could answer “who still uses warehouse?” with a grep instead of a meeting. Deleting things became provably safe. That alone justified the setup cost.

Readable source is a real feature. Parts 2–7 exist because thirty files can be read cover to cover. The findings — the GetUninitializedObject zero-values, options travelling inside the pact, Given(...) never executing, concurrent replay, the silent publish, the two FormatException bugs in the error paths — took me an afternoon each to establish with certainty. In a tool that wraps a native core, the equivalent certainty is a support ticket.

The complete limit list

Collected from across the series, in one place, because an honest adoption decision needs the whole bill:

  • Requests are method + path only. No request bodies (POST/PUT replay with null content), no request headers, no query matchers. Contracts over write-APIs degenerate to “the route exists.” (Parts 2, 6)
  • No matchers at all. No “any string,” no regex, no type-matching, no array semantics. Pact spec's matchingRules — the feature that makes value assertions survive real data — has no counterpart. (Parts 3, 6)
  • Body matching is top-level-only; nested objects are presence-checks at best and reference-equality failures at worst. (Part 6)
  • Given is documentation. No provider-state hooks; the provider seeds one global state satisfying every interaction simultaneously (they replay concurrently). (Part 5)
  • Document-wide dials. Options and the endpoint template apply per-pact, not per-interaction. One lenient interaction makes the whole document lenient. (Parts 3, 5)
  • Error paths with two format-string bugs that turn value-mismatch diagnostics into FormatExceptions; header names compared case-sensitively regardless of IgnoreCasing. (Part 6)
  • The HTTP publisher never checks the broker's response — a failed publish passes your build. (Part 4)
  • Pact-v1-adjacent, not Pact-compliant. Broker-storable, but no matchingRules/metadata, plus a nonstandard options block; assume Pactify-to-Pactify only. Broker auth is a custom Pact-Requester header, not the Authorization schemes real brokers use. (Parts 3, 4)
  • A 2.x-era footprint. Startup-class convention, TestHost 2.2, IRouter-based endpoint, Newtonsoft throughout — workable, but visibly dated against minimal hosting and WebApplicationFactory. (Parts 5, 7)

That list reads brutally in bulk, so let me re-balance it: none of these limits touched the outage that opened this series. The renamed-field regression — the representative disaster of the genre — is caught completely by structural matching over GET endpoints, which is precisely the slice Pactify implements well. The limits define a boundary, not a verdict.

Pactify or PactNet?

My decision rule, having run one and read the other's problem space through it:

Choose Pactify when the seams you're protecting are read-shaped internal APIs between a handful of .NET teams (or one team's monorepo of services), you want the whole team to understand the mechanism, and your contracts are structural. First-touch cost is an afternoon; the pact-in-git workflow (part 4) needs no infrastructure at all. It is also, hands down, the best teaching codebase for the concept — I'd assign parts of it as reading before letting anyone near a broker.

Choose PactNet when you need request bodies and headers in the contract, matchers instead of literals, executable provider states, polyglot consumers (the pact format is the interop layer — Pactify's dialect isn't), spec v3/v4 compliance, or broker workflows with deployment gates. That's not a feature-envy list; each item maps to one of the hard limits above. PactNet buys them by wrapping the Pact foundation's Rust core — the price is that your judgment engine becomes a native binary you'll never read. After this series I consider that a real price, worth paying knowingly rather than by default.

The upgrade path is gentler than it looks: everything organizational — consumer tests as first-class CI citizens, pact publication steps, provider verification gates, the habit of treating the seam as tested — transfers wholesale. You're swapping the recorder and the judge, not the discipline.

The broker maturity ladder

Where teams actually get stuck is never the library; it's the workflow. The rungs, as I've climbed them:

  1. Pacts in the repo. Monorepo folder, git as history, PR review as the change-control conversation. Start here; stay here longer than feels sophisticated.
  2. Pacts as pipeline artifacts. Cross-repo, consumer publishes, provider downloads. Works until the first “which version did we verify?” incident.
  3. A broker. Versioned pacts, a UI both teams bookmark, verification results recorded centrally. This is where “the contract” becomes a shared noun in planning meetings — the cultural payload matters more than the Docker container.
  4. Deployment gates. The full Pact-ecosystem endgame — webhooks triggering provider verification on pact change, can-i-deploy blocking releases until the matrix is green. Pactify has no native rungs here; this level is PactNet-and-friends territory.

Each rung costs an order of magnitude more organizational buy-in than the last, and delivers the same multiple in value — if you have the problem the rung solves. Climbing ahead of your problems is how contract testing initiatives die: a broker nobody consults is a second integration environment with better branding.

When it pays for itself

The tooling was never the expensive part. The consumer-driven contract is an organizational instrument — it moves the veto over provider changes into the consumers' recorded expectations, and pays off exactly where that inversion matters:

  • Multiple teams, independent deploy cadences. The seam breaks precisely when coordination is expensive. This is the home fixture.
  • Churning internal APIs. Contracts shine while shapes are still being negotiated; they're a rename-detector, and renames happen early and often.
  • A shared integration environment everyone hates. Not because contracts replace E2E — they don't — but because they drain the cheap failure classes out of it (shape drift, dead routes), leaving the environment to catch what genuinely needs everything running: emergent behaviour, data flow, timing.

And the honest negative space — where I'd now decline to introduce it: one team owning both sides of every seam (your integration tests already share types; the contract adds ceremony, not information), a public API with unknown consumers (consumer-driven requires enumerable consumers — you want OpenAPI, versioning policy and deprecation windows instead), and genuinely stable interfaces, where a pact is a smoke detector in a stone house.

The most durable change wasn't any pipeline. It was that “will this break checkout?” became a question with a mechanical answer — asked of a JSON document in CI instead of a person in a meeting. Once a team has tasted that, they keep the discipline even when they swap the tool. Which is, I think, the strongest endorsement available for starting with a thirty-file library you can read in an afternoon: Pactify is small enough to fully understand, and consumer-driven contract testing is exactly the kind of idea you want to understand fully before you industrialize it. Credit to its author for shipping the sketch that teaches the discipline — and if the sketch's rough edges bothered you across this series, remember they're all one afternoon and a pull request away. That's what open source is for.

Which CV would you like?

Pick the one closest to the role. Each is two pages and carries the same facts.

Applied AI

Agentic AI & Cloud Engineer · multi-agent systems, RAG and MCP on AWS

Cloud & .NET

C#, .NET 10 and ASP.NET Core microservices on AWS and Azure · Terraform & AWS CDK

Data Engineering

AWS lakehouse · Kinesis, Glue (PySpark), Apache Iceberg, Lake Formation

Everything

All three domains, with the standout project from each