# Contract Change

> Change a shared contract — an API schema, shared types, an event payload, a database column that something else reads — without breaking the consumers that drift from it silently. Use when editing a schema or shared package, when a local mock or stub has diverged from the real service, or when a change works locally and fails against the deployed system.

- Skill: `chinthakat/contract-change` (Agent Skill)
- Install (CLI): `npx skillmds@latest add chinthakat/contract-change`
- Raw SKILL.md: https://api.skillmd.com/api/skills/chinthakat/contract-change/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: chinthakat (https://skillmd.com/u/chinthakat)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/chinthakat/contract-change

---


# Changing a shared contract

A contract is any shape that more than one thing depends on: an API schema, a
shared types package, an event payload, a stored document, a CSV a customer
parses. The failure is always the same — one side changes, the other finds out
in production.

## 1. Enumerate the consumers before you touch it

Write the list. It is always longer than the two you were thinking of:

- The obvious callers — the web app, the mobile app, the service next door.
- **Mocks, stubs and local development servers** that reimplement the contract.
- Test fixtures and recorded responses.
- Generated clients, SDKs and typed hooks.
- Reporting queries, exports, dashboards and the analytics pipeline.
- Anything replaying stored events, which means **old payloads already written**.
- Third parties, who will not be redeployed because you said so.
- Cached bundles and installed mobile apps — versions of your own frontend still
  running in the wild.

That last group is the one that turns a "coordinated deploy" into an outage. You
control the server; you do not control which version of the client is open.

## 2. The local mock is the one that drifts

A hand-maintained local development server, an in-memory fake, or a stub that
mirrors the real schema will drift, because nothing fails when it does. Everyone
develops happily against a contract that no longer matches production.

Two acceptable answers:

- **Generate it.** One source of truth; the mock is derived. Drift becomes
  impossible rather than unlikely.
- **Test it against the real thing.** A contract test that runs the same suite
  against the mock and against a real instance, in CI. Drift becomes loud.

"We'll remember to update both" is not one of them. If neither is affordable
today, at least put a line in the instruction file saying the mirror exists and
must be updated — see `agent-instructions`.

## 3. Additive is safe; removing and renaming are not

| Change | Safe? |
|---|---|
| Add an optional field | Yes |
| Add a new endpoint, event type or enum *producer* side | Usually — check how consumers handle unknown values |
| Make an optional field required | **No** — old clients omit it |
| Remove a field | **No** — something reads it |
| Rename a field | **No** — this is a remove plus an add, and the remove is what breaks |
| Change a type (string to number, seconds to milliseconds) | **No**, and the worst kind: it fails silently and corrupts data |
| Change the meaning of a field, same type | **No**, and undetectable by any tooling |
| Tighten validation | **No** — data that was accepted yesterday is rejected today |
| Add a new enum value | Only if consumers were written to tolerate unknown values |

The last row is worth designing for up front: consumers that reject unknown
enum values make every future addition a breaking change.

## 4. Expand, migrate, contract

Any unsafe change becomes safe as three deploys:

```
1. EXPAND    Add the new field alongside the old. Write both. Read the old.
             Ship. Nothing breaks.
2. MIGRATE   Backfill. Move readers to the new field one at a time. Ship each.
             Keep writing both.
3. CONTRACT  When nothing reads the old field — proven, not assumed — stop
             writing it, then remove it. Ship.
```

Each step is independently deployable and independently revertible. This is
slower than a rename and it is the difference between a change and an incident.

The step people skip is proving nothing reads the old field. Assume something
does until telemetry says otherwise.

## 5. Types passing is not compatibility

A green type check proves that *the code in this repository* agrees with itself
right now. It says nothing about:

- The mobile app version from three months ago.
- A cached JavaScript bundle in someone's browser.
- Events already written to a queue or a log.
- Documents already stored under the old shape.
- A third party's integration.

Compatibility is a statement about data that already exists and clients already
running. Reason about it explicitly; the compiler cannot.

## 6. Version when you must, additive when you can

Versioning an API is expensive — two code paths, two sets of tests, two things
to deprecate — so exhaust additive changes first. Most "we need v2" moments are
three additive changes and a deprecation.

When you do version:

- Version the whole surface or an individual endpoint, consistently.
- Set the end date for the old version when you launch the new one, not later.
- Instrument usage so the shutdown is a fact, not a hope.
- Tell the consumers who are actually using it, identified by telemetry.

For stored documents, prefer a version field on the record and a reader that
handles each known shape — the same idea as `versioned-publishing`, applied to
data rather than content.

## 7. Deprecate visibly

A deprecation nobody notices is not a deprecation:

- Mark it in the schema and in the docs, with the date it goes.
- Log or emit a metric on every use, tagged with the caller.
- Warn in the client — a runtime warning is seen; a changelog entry is not.
- Only remove when usage is zero, and say who the remaining callers are if it
  is not.

## 8. One contract test both sides run

The highest-value artifact here: a suite that the producer runs against itself
and every consumer runs against its expectation of the producer. It fails on the
side that broke, in CI, before anything ships.

Where a full contract-testing tool is too much, a shared schema file plus a test
that validates real recorded responses against it catches most of the same
problems for an afternoon's work.

## Checklist

- [ ] Every consumer enumerated, including mocks, fixtures, stored data and clients in the wild
- [ ] Local mock or dev server either generated from the source of truth or contract-tested
- [ ] Change classified as additive or breaking, honestly
- [ ] Breaking changes split into expand / migrate / contract, each shippable alone
- [ ] "Nothing reads the old field" proven by telemetry, not assumed
- [ ] Compatibility reasoned about for old clients and already-written data, not just the type check
- [ ] Additive exhausted before versioning; end date set when a new version launches
- [ ] Deprecation instrumented and warned at runtime, not only documented
- [ ] A contract test both sides run in CI

