Skip to content
Part IA Easter Term

API Design and Semantic Versioning

APIs as Promises

An API is a contract — a public, documented promise that, given input XX, the system will return output YY. Every caller that integrates with your API builds on that promise. Changing the promise after the fact breaks every caller, potentially causing cascading failures across an ecosystem of dependent systems.

This is why API design requires far more care than internal implementation design. An internal function can be refactored with impunity because you control all its callers. A public API function cannot — once it is published and used by others, changing it is an ecosystem-breaking event.

Semantic Versioning (SemVer): MAJOR.MINOR.PATCH

SemVer is a specification for signalling the nature of changes through version numbers, so that consumers can assess the risk of upgrading:

MAJOR.MINOR.PATCH\text{MAJOR}.\text{MINOR}.\text{PATCH}

BumpWhenExample
MAJORYou made an incompatible, breaking API change. Existing callers will need to modify their code to upgrade.Renaming a public function; removing a field; changing a parameter type; adding a required parameter. 2.4.1 → 3.0.0
MINORYou added new, backwards-compatible functionality. Existing callers can upgrade without changing their code.Adding a new optional parameter with a default value; adding a new endpoint. 2.4.1 → 2.5.0
PATCHYou made backwards-compatible bug fixes. No new functionality; no breaking changes.Fixing a calculation error; patching a security vulnerability without changing the API surface. 2.4.1 → 2.4.2

Rule of thumb: when in doubt, bump MAJOR. Under-versioning (labelling a breaking change as MINOR) is far more damaging than over-versioning (labelling a compatible change as MAJOR). The former breaks consumers unexpectedly; the latter makes consumers unnecessarily cautious but does no harm.

What Counts as Breaking?

Anything that would cause an existing, correctly-written caller to fail:

  • Structural: removing or renaming a function, class, field, or endpoint. Changing a parameter’s type. Adding a required parameter (because existing callers do not provide it).
  • Behavioural: changing a default value. Changing the order of returned results. Throwing a new checked exception. Changing the format of a response field.
  • Semantic: changing the meaning of a return value. If getCount() previously returned the number of active users and now returns total users, this is a breaking change even though the function signature is unchanged. SemVer cannot encode semantic changes; they must be communicated through documentation.

Dependency Constraints

Package managers support different levels of version flexibility:

ConstraintNotation (npm ecosystem)Meaning
Exact1.2.3Only version 1.2.3. No bug fixes. Safest if you distrust the upstream author’s SemVer discipline.
Tilde~1.2.3Allows PATCH-level updates: >=1.2.3 <1.3.0. You get bug fixes but no new features.
Caret^1.2.3Allows MINOR and PATCH updates: >=1.2.3 <2.0.0. The industry default, assuming the author follows SemVer correctly and won’t ship breaking changes in a MINOR bump.

Lockfiles (package-lock.json, Cargo.lock, yarn.lock) record the exact version of every dependency installed in the last successful build. This ensures reproducibility across the team: Alice and Bob both get identical dependency trees, even if a new version was published between Alice’s install and Bob’s.

Postel’s Law (The Robustness Principle)

Jon Postel, one of the architects of the early Internet, stated in RFC 760 (1980):

Be conservative in what you send, be liberal in what you accept.

  • Conservative (Writer / Sender): stick strictly to the specification. Do not send extra fields, non-standard extensions, or “convenient” deviations from the contract. If the spec says a field is optional, do not assume it will always be present just because your current implementation always sends it.
  • Liberal (Reader / Receiver): gracefully handle input that does not perfectly match expectations. Ignore unrecognised extra fields instead of crashing — this is forward compatibility, letting newer systems add fields without breaking older ones.

This principle is why the Internet has survived 40+ years of evolution. TCP implementations vary across operating systems, hardware, and decades — but Postel’s Law ensures they interoperate. A TCP stack that crashes on an unrecognised option byte would cause failures across every network path that passes through a newer router.

Backward and Forward Compatibility

Backward compatibility: newer code can handle requests or data from older code. This is essential during rolling deployments, where old and new versions of a service run simultaneously. A new version of user-service must still understand the message format that the old version sends.

Forward compatibility: older code can gracefully handle data from newer code. This is achieved by ignoring unknown fields (Postel’s liberal reader) rather than crashing on them. In a distributed system, you cannot upgrade every node at the exact same instant — a mix of versions always coexists.

Expected Learning

  • Explain why an API is a contract and why breaking that contract has ecosystem-wide consequences.
  • State the SemVer rules for MAJOR, MINOR, and PATCH, and determine the correct version bump for a given change.
  • Identify whether a described change is breaking (MAJOR) or compatible (MINOR or PATCH).
  • Distinguish exact, tilde, and caret constraints.
  • State Postel’s Law and explain how it enables forward compatibility.
  • Distinguish backward from forward compatibility and explain why both matter in distributed systems.

Past Paper Questions

Example Sheet 2, Question 8(b) tests SemVer: given a specific API change (adding a required parameter), determine the correct version bump and explain your reasoning. Example Sheet 2, Question 9(b-c) tests the deprecation path (covered in the next note) and why breaking a dependency contract causes cascading CI/CD pipeline failures.