skip to content

You set release policy for an organisation shipping both internal libraries and continuously deployed services. How do you decide each one's versioning scheme and release cadence?

level: principalimportance: should knowfreq 30%

answer

  1. ask who reads the number
  2. a compatibility claim needs a reader
  3. artifact version is not API version
  4. majors are a bill to consumers
  5. trains are a fallback, not a default

basics

~20 s

Decide by consumer. Anything other teams resolve through a dependency range gets SemVer plus a written deprecation policy. Anything deployed only by the team that owns it gets a build identifier and continuous release, because a compatibility claim nobody reads is pure ceremony.

solid answer

~50 s

I start from one question per artifact: who reads this number, and what decision do they make with it? A shared library resolved by twenty other builds needs SemVer, because a resolver acts on the fields — and it needs the rest of the contract too: a declared public surface, a deprecation window, and API-diff checks in CI so the bump is verified rather than self-graded. A service deployed by its owning team has no such reader; a build or commit identifier is more honest and removes a recurring debate that produces nothing. I also keep the *API* contract version separate from the *artifact* version, because HTTP or message contracts change on a different schedule from packages. On cadence, continuous release is the default and a release train is what I fall back to when consumers cannot absorb changes continuously — a coordinating cost I want to justify, not inherit. Then I budget majors: each one is a migration bill charged to every consumer.

go deeper

for a junior

Take away the core rule: a version number is written for whoever reads it. Libraries other teams depend on need SemVer; a service only your own pipeline deploys mostly needs a way to identify the build.

for a middle

Be able to justify a scheme from the consumption model rather than convention, and explain why a service's HTTP contract version and its artifact version are different things that change on different schedules.

for a senior

Show the enforcement layer: declared public surface, deprecation windows, API-diff checks in CI, and versions derived by the pipeline rather than typed by hand at release time.

for a principal

Own the economics. Majors are migration bills charged to other teams, so budget and batch them, require a migration path, and track adoption. Keep the mandate to a handful of enforceable rules and let teams choose the rest.

## The framing question Every versioning argument I have watched go badly was really an argument about audience. So the policy starts with one question asked per artifact class: **who reads this number, and what do they do differently depending on its value?** Three answers cover almost everything: 1. **A dependency resolver in someone else's build.** The number must carry compatibility. SemVer, non-negotiable. 2. **A human deciding whether they are still supported.** The number should carry time and support window. A calendar scheme fits. 3. **Only our own pipeline and our own operators.** The number needs to identify a build. A serial or commit-derived identifier is the honest choice. If no answer to "what do they do differently" exists, the scheme is ceremony, and ceremony has a cost: recurring debates, mis-graded bumps, and a signal that consumers eventually learn to ignore. ## Libraries: SemVer is the cheap part Saying "our libraries use SemVer" is easy and nearly meaningless on its own. The commitments that make it real are: - **A declared public surface.** Without it, "breaking" is a matter of opinion and every team draws the line differently. - **A deprecation window.** Removal in the next major is not a policy; "deprecated for at least one minor release and one calendar quarter before removal, with the replacement named in the deprecation message" is. - **Verification in CI.** Authors grading their own compatibility get it wrong under deadline pressure. API-diff tooling that compares the built surface against the last release and fails when the change outranks the bump on the branch turns the policy into a gate. - **Machine-readable release notes.** Consumers upgrading twenty dependencies do not read prose. A changelog derived from typed commits, with breaking changes in their own section, is what makes a major bump actionable. ## Services: resist SemVer theatre For a service deployed by the team that builds it, the SemVer fields have no reader. There is no resolver; there is a pipeline that takes the artifact just built and runs it. What operators actually need is provenance — given this running instance, which commit produced it — and that is a build identifier or a commit-derived version. The common objection is that the version communicates significance to stakeholders. That is a release-notes job, not a version-number job, and conflating them is how a team ends up unable to make an incompatible internal change without a major bump nobody wants to explain. ## Separate the API version from the artifact version This is the highest-leverage rule in the whole policy. A service's HTTP or message contract changes for different reasons, on a different schedule, and with different consumers than its deployable artifact. Fused together, you get two failure modes: a contract change blocked because the artifact major looks alarming, and an artifact bump that alarms clients who see nothing change in the contract. Kept separate, `v2` of the API and build `2024.31.4` of the service move independently, which is what they actually do. ## Cadence: continuous by default, trains by exception Continuous release — every merge that qualifies produces a release — is the default because it minimises batch size, which is the variable that drives both risk per release and time to fix. A release train (fixed cadence, whatever is ready goes) is the fallback, and it is a fallback for real reasons: - consumers who cannot absorb changes continuously (customer-installed software, regulated environments, coordinated multi-component upgrades) - release work that is genuinely expensive per release: certification, translation, physical distribution, external audit - a coordination boundary — several components that must ship as a set What I try to prevent is a train adopted because releasing is painful. That is a signal to fix the pipeline, not to release less often; batching is how a painful release becomes a *riskier* painful release. ## Budget your majors A major bump is a bill charged to every consumer: their engineering time, their testing, their scheduling. Across an organisation that bill is real money, and it is paid by teams other than the one that decided to send it. So the policy treats majors as scarce: - Batch breaking changes rather than dribbling them out; one migration a year is cheaper for consumers than four. - Require a migration path — a codemod, a compatibility shim for one release, or at minimum a mechanical upgrade guide. - Track adoption. A major that half the consumers never adopt has not replaced the old line, it has forked your support burden. ## What I would actually mandate Keep the mandate small and enforce it, rather than publishing a long standard nobody reads: 1. Anything published for another team to resolve uses SemVer and declares its public surface. 2. Nothing re-publishes an existing version with different content, ever. 3. Deprecation precedes removal by a stated window. 4. The version is derived by the pipeline from committed intent, not typed by a human at release time. 5. Support windows for older lines are written down before anyone needs them. Everything else — which calendar format, whether tags carry a `v` prefix, how the changelog is grouped — I would let teams choose. Uniformity there buys nothing and spends the political capital I need for the five rules that matter.

  • A team argues their internal service must use SemVer for consistency with the libraries. How do you respond?
    I ask what decision anyone makes differently between 3.4.1 and 4.0.0 of that service. If nobody resolves it through a range, the fields have no reader and the bump debate produces no value. Consistency of *scheme* is not a goal; consistency of *principle* — the number serves its reader — is. I would rather spend the standardisation budget on the rules that bite, like immutability and deprecation windows.
  • How do you know whether your organisation's SemVer discipline is actually working?
    Measure the failures, not the compliance. Two signals: incidents caused by a minor or patch upgrade, which mean breaking changes are shipping under-graded; and consumer adoption lag on majors, which means migrations cost more than the value they deliver. Both are observable without asking anyone to self-report, and both point at a specific fix — better API-diff gating, or better migration tooling.
  • When is a release train the right choice rather than continuous release?
    When something outside the pipeline makes each release expensive or coordinated: customer-installed software, regulated or certified environments, translation and documentation cycles, or components that must ship as a matched set. The test is whether the cost is inherent to the consumer's situation. If the real reason is that releasing is painful internally, the answer is to fix the pipeline — batching makes a painful release riskier, not safer.
  • What is the strongest argument for keeping a service's API contract version separate from its artifact version?
    They have different consumers and different change drivers. Clients care about the contract and never see the artifact; operators care about which build is running and do not track the contract. Fused, a needed contract change gets blocked by an alarming-looking artifact major, or an artifact bump panics clients for whom nothing changed. Separated, each moves at its own natural rate.

saying these in an interview costs you the question

  • Mandating one scheme organisation-wide for consistency
  • Bumping a service major to signal a big release
  • Declaring SemVer without declaring the public surface
  • Batching releases because releasing is painful
  • Treating a major bump as free to consumers

context