If an API falls over in the forest, does it make a sound?

If you would prefer a visual / narrated version of this blog post, feel free to watch the recording (recording link to follow)

This talk was performed live at APIDays London 2026, and is dedicated to my dad, Younus Nabi (1957–2025). He passed away last year, and he’s the reason I’m here today. The day before the talk would have been his birthday.

My Dad got into computers late in life, a lesson that you can pivot your career at any time. As he set up his study, I procured a 3 legged stool from a reclamation yard, painted it with my mum, where it became my favourite perch, where I would watch, wide-eyed, as my Dad was basked in a CRT glow. I’ll never forget a plaque he had on the door: “Computers will never replace human stupidity.” It has stuck in my head through my whole life and into my career, from studying artificial intelligence and cybernetics at university, through accessibility testing, consultancy, and now advocacy.

When he fell ill last year, I was due to speak at APIDays New York that week. He emplored me to go, he wanted me to be out there sharing stories. It would be the last time we properly spoke. So thanks, Dad, for everything but mainly giving me the thirst for knowledge. Hold on to that saying of his “Computers will never replace human stupidity.”, because it turns out to be the point of this whole post.

If a tree falls in a forest…

You all know philosophy’s favourite thought experiment. If a tree falls in a forest and no one is around to hear it, does it make a sound? The answer depends on what you mean by “sound”: the vibration in the air, or someone actually hearing it.

Software has exactly the same split.

  • The vibration is the failure. A 500, a timeout, a field renamed on a Friday afternoon, a quietly changed enum. It happens regardless. Logs fill up and dashboards turn red, if anyone is watching.
  • The hearing is the impact. A customer who can’t check out, a partner integration that silently stops working, an engineer paged at 3am. That is the “sound”.

In our systems, the sound is impact. The hard part isn’t the failure, it’s knowing who’s listening.

I kicked off the session with a quick show of hands. Left hand up if you provide an API, right hand up if you consume one. Loads of people had both hands up, and that’s the point: almost all of us are providers and consumers. Then again: left hand if you’ve caused an API failure, right hand if you’ve been on the receiving end of one. Both of mine went up. Keep that feeling in mind, because that’s who these stories are about.

Decoupled systems gave us decoupled teams

Decoupled, distributed systems were a huge win. But they also gave us decoupled, distributed teams.

We went from one codebase, one team, one release and one shared mental model, to multiple, for example, Orders, Payments, Identity, Inventory, Shipping and Notifications, each owned by a different team. Nobody holds the whole picture, yet all of it is expected to converge, at any given moment, into one coherent product.

The user’s expectations from the outside don’t change depending on how your system is architected.

Conway’s Law tells us that systems mirror the communication structures of the organisations that build them. The gaps between teams become the gaps between services, and that’s where friction lives.

From the trenches #1: a new journey, on a heritage backend

A while back I worked on a brand new UI journey. A GraphQL UI service, calling through several Spring Boot services and a single Scala service, all of them talking to a heritage backend. (I’m reframing “legacy” as “heritage” these days.)

Our teams all sat in the same bank of desks, apart from the heritage backend team, who were in another country (thankfully on the same time zone). We wanted to give teams the autonomy to develop independently, which is why one team chose Scala and another chose GraphQL for the UI service: the right technology for their particular purpose.

That autonomy came with challenges. This was a little before the real rise of Swagger and OpenAPI, so we didn’t have a coherent way to talk about our systems. The backend team also worked to a different methodology, and we only found out about their changes when they landed. Change arrived as a surprise, not a conversation.

As testers, we built service stubs so our teams could work independently, and we’d periodically run them against the heritage backend to look for divergence. It helped, but it was still after the fact.

The lesson for me was that we were reactive to change, not proactive. We needed to see change coming, and have the conversation before it happened, not after.

That’s friction between teams. So let’s look at who actually feels it.

Three stories of friction

There are three characters in every API story: the provider who needs to change the API, the integrator trying to build on it, and the end user who never sees it but lives with the consequences.

Story A: The Provider

“I want to change my API, but who will I impact, and how?”

Say you want a simple rename on GET /customers/42 name becomes fullName. Who breaks? The web app? Mobile app versions you can’t force-upgrade? A partner? That nightly batch job from 2019?

Hyrum’s Law says it’s worse than that: with enough users, every observable behaviour of your API will be depended on by somebody. Your spec describes what you promised, not what people actually rely on.

Without a view of who consumes what, every change is a guess. So teams either freeze, and the API slowly ossifies, or they ship and wait to hear the crash. The falling tree.

Story B: The Integrator

“The docs say one thing. The API does another.”

The integrator came to learn capabilities, features and business workflows. Instead, because the docs are missing or wrong, they end up reverse-engineering the boundaries:

  • Which fields are really required
  • The real error shapes
  • Undocumented enum values
  • Null vs. missing
  • Pagination quirks
  • Auth scopes and rate limits

That’s trial and error against the real thing, or worse, against production. It’s time spent on surface area instead of value, and whatever they learn stays as tribal knowledge the provider can’t see. Which feeds straight back into Story A.

Story C: The End User

“I just wanted to pay for my shopping.”

The end user just wants to pay. Behind that one tap is a spider web of calls: an API gateway, basket, pricing, payments, fraud check, stock and loyalty.

When one strand fails (say the fraud service changed a response and nobody downstream knew), the user doesn’t get a helpful schema error. They get “something went wrong”, and they shop somewhere else. They don’t have time to wait for you to fix your system; they’ll vote with their thumbs. And they’ll never know why.

From the trenches #2: live in production. Nobody came.

We’d built a brand new credit loan system. Mostly greenfield, a modern cloud architecture, still integrating with some heritage systems. On go-live day we were all huddled on the couch with our laptops, watching the screens, waiting for anything to come through the logs.

Nobody came. Total anti-climax.

So we spoke to the business, and the next day they agreed to send someone into a store to actually buy something. It was live, but was it working? Nobody could hear that API.

It probably cost the business £500 to £1,000 to buy a piece of furniture. But it meant the hundreds of thousands of pounds spent on the project weren’t sitting idle while we waited for logs. The lesson: if you build a system, make sure there’s traffic you can monitor. If you don’t know when real traffic will come, put some through it yourself.

Then, thankfully not straight away, came our first real incident. We’d moved from on-prem to the cloud, onto Lambdas, so calls went through ephemeral services and on to backend services via API gateways. Correlating logs across all of that was impossible: we couldn’t follow one client request through every part of the system it touched. We very quickly implemented distributed tracing. When it finally made a sound, we couldn’t tell where it came from.

It only makes a sound when it lands on someone

So, does it make a sound? An API with genuinely no consumers can fall silently, and nobody cares. But we rarely know that for sure. We find out who was standing underneath when they shout: an incident, a ticket, an angry partner. The worst possible time to find out.

Descriptions describe. Contracts capture intent.

What’s missing in all three stories is intent: a shared understanding of how our systems communicate, and who relies on what.

OpenAPI and AsyncAPI are brilliant, but they are the provider’s view of what’s possible. They’re abstract (types, shapes and rules), they say nothing about who uses what, and they can drift from the implementation.

A consumer contract is each consumer’s view of what they need. It’s concrete: real requests and real responses, a record of intent per consumer and per field, verified against the real provider so it can’t drift.

As Matt Fellows (Pact core maintainer and PactFlow co-founder) puts it: “Schemas are abstract, contracts are concrete.”

Contract testing: a system of record for intent

Using Pact as the example, the workflow looks like this:

  1. Consumer tests: consumers state what they need, in tests against a mock provider.
  2. Contract: those expectations become a contract. Intent, captured by machines.
  3. Broker: a system of record. Who consumes what, at which version, deployed where.
  4. Provider verification: the real provider replays every contract against its real implementation. Honoured, or not?
  5. can-i-deploy: only ship what’s compatible with what’s actually running.

Pressure on the design, applied by machines, before anything reaches production.

From the trenches #3: unknown unknowns

Remember that loan system nobody came to? It got popular. We were moving out of the POC phase and rolling it out to more retailers.

We had an MI (management information) system built on Postgres logical replication. Whenever something changed in the database (a new loan application, say), a changeset was captured as JSON with wal2json and pushed into Kinesis. We transformed it into a known structure, enriched it with extra data, and dropped it into an S3 bucket, where the MI report would run.

The first problem was that we didn’t actually know when the MI report wasn’t running. Then we were asked to onboard a batch of new retailers directly via the database. That large import produced a changeset over 1 MB, which is Kinesis’ record limit. No messages reached the MI report, which meant lots of angry retailers who weren’t being paid.

The fix was better logging, so we knew when the MI report wasn’t running, plus logic to split oversized changesets.

And then déjà vu. Making a loan application kicks off several checks, fraud checks among them, and they’d sometimes take longer than API Gateway’s ~30-second timeout. Not a bug as such, more a quirk of how our systems communicated. Also discovered in production.

You don’t know what you don’t know, and these unknown unknowns are going to bite you. So how do you manage them? You can’t test for them up front. But once one bites, encode it: a test, an alert, a guard. Never learn it twice. And invest in logging, so the next unknown at least makes a noise you can hear.

Contract tests do exactly that for the interactions between services. They turn “found out in production” into “found out in the pull request”.

Friction is a feature, if it arrives at the right time

Friction isn’t the enemy, it’s information. What matters is when it arrives, and who it arrives with.

Friction found late (in shared test environments, or in production) is expensive and vague. Something is broken, but for whom? Everyone gets pulled into the incident channel, and you’re into rollbacks, hotfixes and big-bang releases.

Friction found early (on a developer’s machine, in the pull request) is cheap and precise. Field by field, consumer by consumer, with only the people who need to be in the conversation. Pressure is applied to the design, before it ships.

Pin-point precision: who relies on which field?

Here’s an illustrative example of what a broker can give you for the response of GET /customers/{id}: for each field, which consumers actually rely on it.

  • Change name? That’s Web, Mobile and Email, but not Loyalty.
  • Change address.postcode? Mobile and Loyalty.
  • phone? Nobody. Remove it today.

Compare that with a message in a big channel asking “is anyone using phone?” and hoping. With contracts in a broker, this view comes from real consumer tests, not tribal knowledge or guesswork. Right conversations, right time, right people.

Machines surface friction. Humans decide.

This isn’t about replacing people, and that feels especially relevant in today’s AI revolution.

Machines do the relentless part. They detect every incompatible change, pinpoint the field, consumer and version, and block unsafe deployments automatically. They never forget, and they never get bored.

Humans do the judgement. They talk to the right team at the right time, weigh business priority and risk, choose the migration strategy, and agree timelines.

Or, as my dad put it: “Computers will never replace human stupidity.” And, I’d add, human judgement.

Expand & contract, in practice

Here’s our name → fullName rename, done gracefully:

  1. Expand: add fullName alongside name. Provider verification proves we still honour every existing contract.
  2. Migrate: consumers move to fullName at their own pace, and publish new contracts.
  3. Observe: the broker shows which deployed consumers still rely on name. In our example, that’s loyalty v3.2 in production. One conversation, with one team.
  4. Contract: once no deployed consumer needs name, remove it, and can-i-deploy agrees.

Only got a couple of internal consumers? A coordinated big-bang release works too. Contracts still tell you who “everyone” is.

The question every pipeline should ask

This is the moment the machine says “hold on”. Before customer-service ships, the pipeline asks the broker: can I deploy?

$ pact broker can-i-deploy \
--pacticipant customer-service \
--version $GIT_SHA \
--to-environment production
Computer says no ¯\_(ツ)_/¯
CONSUMER | C.VERSION | SUCCESS?
web-checkout | 8.1.0 | true
mobile-app | 5.4.2 | true
loyalty-service | 3.2.0 | false

Web and mobile are fine; loyalty-service 3.2.0 isn’t. Computer says no.

If you’ve ever watched Little Britain, you’ll spot the nod: the Pact Broker will tell you “computer says no” (or, happily, “computer says yes”).

  • It answers with facts, not hope: based on verified contracts, not a green staging run.
  • It knows what’s deployed where: it checks against the versions actually running.
  • It names the conversation: which consumer, which version, which interaction.

That’s the process working. The friction surfaced before any user felt it, and now a human decides what to do about it.

Back to our three stories

With intent captured as contracts, here’s how our three characters fare:

  • A · The Provider knows who they’ll impact, and how: field by field, before they merge.
  • B · The Integrator gets living, verified examples of real usage, and a provider that honours them.
  • C · The End User never notices. Change rolls out without breaking their journey. That’s the nirvana: they get the value of your new features without losing the value they already have.

That’s harnessing friction: at the right time, in the right way, with the right people. And it leaves everyone a much smaller mental model to hold in their heads when changing things safely.

Final thoughts: does it make a sound?

If an API falls over in the forest and no one is using it, does it make a sound? Only if someone’s listening. Contracts tell you who, before it falls.

If you want to stop waiting for the crash to find out who’s standing underneath your APIs:

✅ Capture intent, not just descriptions: let consumers state what they actually need

✅ Use a broker as your system of record for who consumes what, at which version, deployed where

✅ Verify providers against every contract, on every change

✅ Ask can-i-deploy in every pipeline, so you only ship what’s compatible with what’s running

✅ Evolve with expand & contract, and let the broker tell you when it’s safe to remove things

✅ Encode every lesson as a test, alert or guard, so you never learn it twice

✅ Let machines surface friction, and let humans decide what to do about it

Want to know more?

Feel free to reach out:

Thanks for reading, and thanks Dad.

Published by

Unknown's avatar

YOU54F

I have been a Software Test Engineer for 11+ years now, starting off in Accessibility Testing at a large UK Banking organisation. I have since worked with financial / medical healthcare / betting / telecommunication providers testing software and along the way helping some migrate from traditional software development methodologies to a leaner Agile based approach. I now work for a consultancy company, providing insight into tools / technologies / approaches to ensure we are testing the right things, at the right place in the stack. Less of my time these days is spent writing test code, and more time championing test processes with our developers, who have picked it up and hit the ground running.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.