skip to content

In a shared contract repository, how can generated client stubs drift from the contract version they claim to implement?

level: seniorimportance: should knowfreq 38%

answer

  1. stubs are a function of two inputs
  2. pin the contract and the generator
  3. generated code is a build output
  4. regenerate in CI and diff
  5. stamp both versions on the artefact

basics

~20 s

Drift enters wherever generation is not reproducible from a pinned input: stubs built from a working copy instead of a tagged contract, a different generator version, hand-edited generated files, or an artefact whose stamped version is not the one it was built from.

solid answer

~40 s

A published stub is only trustworthy if it is a pure function of two pinned inputs: the contract at a named version and the generator at a named version. Drift is any break in that function. The common breaks are generating from an uncommitted working copy, generating from a branch and publishing it under the release version, upgrading the generator without republishing, checking generated code into a repository and editing it by hand, and consumers pinning a stub artefact whose contract version no longer exists upstream. The defence is to make generation reproducible and verified: regenerate in continuous integration from the tagged contract with a pinned generator, fail the build on any diff, stamp both versions onto the published artefact, and treat generated code as a build output rather than as source.

code

pseudocode · 14 lines
pseudocode
# runs on every build of the contract repository
expected = generate(
    contract = checkout(CONTRACT_TAG),
    generator = GENERATOR_VERSION)

actual = read_tree("generated/")

if actual != expected:
    fail("generated stubs are stale: regenerate from " + CONTRACT_TAG)

publish(expected,
        contract_version = CONTRACT_TAG,
        contract_digest = digest_of(checkout(CONTRACT_TAG)),
        generator_version = GENERATOR_VERSION)

go deeper

for a junior

Remember that generated stubs are produced from a specific contract file by a specific tool version, and that editing them by hand is not safe.

for a middle

Explain generation as a function of two pinned inputs and name the ways each input goes unpinned — working copies, branches, unpinned generator versions, hand edits.

for a senior

Show the controls you would actually run: regenerate-and-diff in CI, provenance stamped on the artefact, generator upgrades as their own reviewed release, and how you recognise drift in production.

for a principal

Own the release discipline of the contract repository and decide what the organisation gains from reproducibility against the cost it imposes on every consuming team.

## Drift is a broken function Think of stub generation as a function: `stubs = generate(contract@version, generator@version)`. A stub artefact is trustworthy exactly when it is the output of that function for the inputs it claims. **Drift is any way that equation stops holding** while the artefact keeps its label. Framing it this way is more useful in an interview than listing incidents, because every incident is one of the inputs going unpinned. ## Where the equation breaks 1. **The contract input is not pinned.** Someone generates from a working copy with an uncommitted edit, or from a branch, and publishes under the release version. The artefact is real code, built from a contract that exists nowhere. 2. **The generator input is not pinned.** The same contract, compiled by two generator versions, can emit different accessors, different naming for an edge case, and different handling of an unknown value. Two consumers then behave differently while both claim the same contract version. 3. **The output is edited.** Generated code checked into a repository invites a small hand fix — a nullability tweak, a convenience accessor — and from then on regeneration would revert it, so nobody regenerates. 4. **The stamp lies.** The artefact carries a version label applied by the release job rather than derived from the inputs, so a rebuild for an unrelated reason republishes different bytes under the same label. 5. **The consumer is pinned to a version upstream has moved past.** Not strictly drift in the artefact, but the same operational symptom: a service implementing a contract nobody can reproduce. ## Detection and prevention - Make generated code a **build output**, never a source file. What cannot be edited cannot be hand-edited. - If it must be checked in — and there are real reasons, such as consumers without the toolchain — then **regenerate and diff in continuous integration** and fail on any difference. This is the single highest-value control. - **Pin the generator version in the contract repository** and treat its upgrade as a release of its own: regenerate everything, review the diff, republish. - **Stamp provenance into the artefact**: contract version, contract content digest, generator version. A consumer can then answer "which contract does my service actually implement?" from the artefact alone. - **Compare digests at the boundary** where it matters: a producer that advertises the contract digest it serves, and a consumer that logs the digest it was generated from, turn a silent mismatch into a startup warning. ## What a drifted stub looks like in production The symptom is rarely a crash. A stub built from a contract that had one extra field will simply never populate it. A stub built by an older generator may not know about a newly declared enumeration member and map it to the unknown case. The reports that arrive are "this value is always empty for one client" or "one consumer classifies these records differently" — per-consumer inconsistency with no error rate to chase. That is why provenance stamping pays for itself: it converts a data-quality investigation into a version comparison. ## The organisational half A contract repository has a release discipline whether or not anyone writes it down. Making it explicit is short: - a contract change lands with a version bump, and the tag is the only thing generation reads; - the publish job is the only thing allowed to publish stubs, from a clean checkout of that tag; - generator upgrades are separate releases with their own diff review; - every published artefact carries its two input versions; - consumers depend on published artefact versions, never on a build from a colleague's machine. None of that is exotic, and an interviewer asking this question is usually checking whether you have seen the failure that these rules exist to prevent — a consumer running code that no contract in the repository can reproduce.

  • Two teams generate from the same contract version and get different code. What do you check first?
    The generator version and its options, because those are the other input to the function. The same contract compiled by two generator versions can differ in accessor shape, identifier mangling and unknown-value handling. If both match, check that both really resolved the same contract content — compare digests, not version labels, since a mutable tag can be moved.
  • Is checking generated code into version control always wrong?
    No. It helps consumers who cannot run the toolchain, makes review of a contract change concrete, and keeps builds working offline. It is only wrong without a gate: if continuous integration regenerates from the pinned contract and fails on a diff, checked-in generated code is verified rather than trusted, and hand edits cannot survive.
  • What is the production signature of a drifted stub?
    Per-consumer inconsistency with no error rate: one client's records always miss a field, or one consumer classifies an enumeration value as unknown while others do not. Because nothing fails, it arrives as a data-quality report. Provenance stamped on the artefact turns that investigation into a comparison of two version numbers.

saying these in an interview costs you the question

  • Treats the generator version as irrelevant to the output
  • Hand-edits generated files and expects regeneration to preserve it
  • Publishes stubs built from a working copy under a release label
  • Assumes a matching version label proves matching contract content
  • Expects drift to announce itself as a decode error