skip to content

Semantic Versioning (SemVer)

MAJOR.MINOR.PATCH is a promise about compatibility, with pre-release and build metadata on the side. The interesting part is what counts as a breaking change to a public API, since that judgement is what makes automated upgrades safe or not.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

6

In Semantic Versioning (SemVer), a library bumps its published version from 2.3.5 to 2.4.0, and later from 2.4.0 to 3.0.0. What does each of those two jumps tell a consumer about what changed, and why does that difference matter when deciding whether to upgrade?

level: juniorimportance: must knowfreq 80%

answer

  1. MAJOR.MINOR.PATCH = breaking.feature.fix
  2. higher position resets lower positions to zero
  3. it's a promise, not enforced
  4. Hyrum's Law - undocumented behavior becomes API
  5. caret range trusts the MAJOR boundary

basics

~10 s

SemVer is MAJOR.MINOR.PATCH. 2.3.5→2.4.0 (MINOR) means new features were added, safe to upgrade. 2.4.0→3.0.0 (MAJOR) means something incompatible changed - your code might break, so check the changelog first.

solid answer

~40 s

SemVer encodes three promises in one number: PATCH bumps for backward-compatible bug fixes, MINOR bumps for backward-compatible new functionality, and MAJOR bumps for changes that break the public API. 2.3.5→2.4.0 is a MINOR bump - the maintainer is promising nothing that depended on 2.3.x should break. 2.4.0→3.0.0 is a MAJOR bump - it's an explicit signal that some public-facing contract changed (removed function, changed signature, altered default behavior) and consumers must read the changelog and likely change their own code before adopting it. This distinction is what lets tools like npm, Cargo, or Gradle auto-upgrade MINOR/PATCH releases safely while gating MAJOR upgrades behind a human decision.

go deeper

for a junior

Can state the three-part format and correctly say which letter means what; doesn't need to know Hyrum's Law or resolver internals yet.

for a middle

Should connect the number to caret/tilde range behavior in their day-to-day package manager and know that a PATCH bump can still break things in practice.

for a senior

Should articulate SemVer as an unenforced social contract, explain Hyrum's Law risk, and describe how their team's CI still runs full tests on dependency bumps rather than trusting the version alone.

for a principal

Should discuss how to design deprecation/removal policy across an org's own published packages so internal consumers can trust the org's MAJOR/MINOR/PATCH signals, and where automated tooling fits into acceptable risk.

## What the three numbers mean **Semantic Versioning**, formalized at semver.org by Tom Preston-Werner, turns a version string into a machine-and-human-readable promise about compatibility rather than an arbitrary label. The format is `MAJOR.MINOR.PATCH`, and each position has a strict, independent meaning: - Increment **PATCH** when you ship a backward-compatible bug fix. - Increment **MINOR** when you add backward-compatible functionality. - Increment **MAJOR** when you make an incompatible change to the public API. Crucially, when a higher position increments, everything below it resets to zero — so `2.4.0` followed by a fix becomes `2.4.1`, not `2.4.0.1`, and a breaking change from `2.4.1` becomes `3.0.0`, not `2.5.0` or `2.4.2`. ## The question each position answers Mechanistically, the three numbers exist to answer three different questions a consumer's build tool asks before pulling a new version: | Position | The question it settles | |---|---| | PATCH | 'is this safe to take automatically because nothing changed except a bug got fixed?' | | MINOR | 'is this safe to take automatically because only additive capability arrived?' | | MAJOR | 'must a human look at this before it's taken, because something I depend on might now behave differently or not exist?' | This is why package managers structure their default version ranges around this contract: a caret range like `^2.3.5` in npm will happily accept `2.4.0` or `2.9.9` but never `3.0.0`, precisely because the SemVer contract says only the leading non-zero digit protects against breakage. ## Why it exists at all The reason this exists at all is **coordination cost at scale**. Before SemVer became a de facto standard (popularized around 2010-2011 and now the default in npm, Cargo, Go modules, and most language package managers), version numbers were largely marketing artifacts — a vendor could jump from 4.2 to 5.0 for a UI refresh with zero API impact, or silently break callers in a 'minor' 2.1 to 2.2 release. That made automated dependency resolution effectively impossible: a resolver couldn't decide which upgrades were safe without a human reading every changelog. SemVer replaces that ambiguity with an explicit, checkable contract, which is what makes tools like Dependabot, Renovate, `npm update`, or Cargo's default resolver able to bump dependencies unattended for MINOR/PATCH and only open a distinct, higher-risk PR for MAJOR bumps. ## The trade-off The trade-off is that SemVer is a **social contract**, not something the runtime enforces. Nothing stops a maintainer from publishing `1.4.2` that quietly changes a function's return type, or from treating a 'MINOR' release as license to deprecate and immediately remove something. The specification only constrains the *meaning* of the number; compliance depends entirely on the maintainer correctly classifying their own change, which requires knowing the full public API surface, including behavior that was never formally documented but that users came to depend on (**Hyrum's Law**). Teams that treat SemVer as a hard guarantee and blindly auto-merge MINOR/PATCH bumps in CI can get burned by a maintainer's classification mistake, so mature pipelines still run the full test suite against upgraded dependencies rather than trusting the version number alone. ## Failure modes Failure modes show up in a few recognizable shapes in production. 1. The most common is the **'accidental breaking patch'** — a maintainer fixes what they believe is a bug, but some consumers were relying on the old (buggy) behavior, so the PATCH release breaks them; this is functionally a MAJOR change mislabeled as a PATCH. 2. Another is **'MINOR creep'**, where a team ships a new required configuration option or changes a default value under a MINOR bump because it 'adds a feature', when in fact existing callers now behave differently — a classic Hyrum's-Law violation. 3. A third is **transitive breakage**: your direct dependency correctly follows SemVer, but one of *its* transitive dependencies didn't, and an automated MINOR bump three levels down the tree breaks your build even though every intermediate package technically obeyed the spec. ## Where it shows up A concrete real-world illustration is the npm ecosystem's `^` (caret) default: when you run `npm install lodash`, package.json records `^4.17.21`, meaning npm will auto-resolve any 4.x.y >= 4.17.21 but will never silently take 5.0.0. This single convention — built entirely on trusting that lodash's maintainers apply MAJOR bumps correctly — is what allows `npm install` in a fresh CI job to pull slightly newer PATCH/MINOR releases automatically without anyone opening a PR, while a jump to lodash 5 requires an explicit, reviewed change to package.json. The entire automated-upgrade ecosystem is built on the assumption that this contract mostly holds, with test suites as the safety net for the cases where it doesn't.

  • If a maintainer removes a deprecated function in what they publish as a MINOR release, what's actually wrong with that, precisely?
    Removing any part of the public API is an incompatible change by SemVer's definition, regardless of how long it was marked deprecated - deprecation is a warning, not a version-number exemption. It should have been a MAJOR bump; publishing it as MINOR breaks the caret-range contract consumers and their tooling rely on, causing an unreviewed `npm install` to fail or misbehave.
  • Why might a resolver treat a caret range differently for a package still below 1.0.0?
    SemVer explicitly exempts the 0.x.y range from the full compatibility promise, so tooling narrows the caret's safe zone to only PATCH-level bumps for 0.x packages instead of the full MINOR range it allows once a package hits 1.0.0. This mirrors the spec's own statement that 0.x APIs shouldn't be considered stable.
  • How does a team typically isolate whether a test failure came from a package's own breaking change versus one of its transitive dependencies?
    They generally can't tell from version numbers alone - both look like 'tests failed after upgrade.' Practically, teams bisect the lockfile, upgrade one dependency at a time, or read the failing package's own changelog to see if it bumped something below it in the tree.

Think of SemVer like a restaurant's menu-change policy: PATCH is 'same recipe, fixed a typo on the label', MINOR is 'added a new optional topping, your usual order still works', MAJOR is 'we changed the recipe itself - if you cared about the old ingredients, check before you order again.'

saying these in an interview costs you the question

  • Says MAJOR/MINOR/PATCH are just marketing/arbitrary numbers
  • Thinks any version bump higher than PATCH is fine to auto-merge without tests
  • Doesn't know a removed deprecated function is still a breaking change
  • Believes SemVer is enforced by the package registry/tooling itself
  • Can't explain why lower numbers reset to zero on a higher bump

context

open as a page

A published library's public API compatibility promise under Semantic Versioning covers more than just function signatures. Besides removing or renaming an exported function, name two other kinds of changes that count as 'breaking' under SemVer and explain why each breaks the promise even though no code was deleted.

level: middleimportance: must knowfreq 85%

basics

~20 s

Breaking changes aren't just deleted functions. Changing what a function returns, tightening validation so old inputs now error, or changing a default behavior can all break callers even though the function still exists - because existing code that worked now behaves differently or fails.

open as a page

A team runs Dependabot/Renovate configured to auto-merge any dependency upgrade that stays within its existing SemVer range (i.e., MINOR/PATCH bumps only) once CI passes. Six months in, a MINOR bump auto-merges and a production incident follows. Walk through why SemVer-based auto-merge can still produce this outcome, and what would reduce the risk without abandoning automation entirely.

level: seniorimportance: must knowfreq 70%

basics

~20 s

SemVer numbers are a promise, not a guarantee - a maintainer can mislabel a change, or your tests might not cover the exact behavior that changed. Auto-merging on version number alone can still let a real break through if your CI doesn't actually exercise that code path.

open as a page

Given the two version strings `1.5.0-beta.2` and `1.5.0+build.20260104`, what does the `-beta.2` suffix versus the `+build.20260104` suffix each mean under the SemVer spec, and which one (if either) affects how two versions compare for precedence?

level: middleimportance: should knowfreq 55%

basics

~20 s

The dash part (-beta.2) means it's a pre-release, not yet the real 1.5.0, and it does affect ordering - it sorts before 1.5.0. The plus part (+build...) is just extra build info like a commit hash, ignored when comparing versions.

open as a page

SemVer defines a special rule for the 0.y.z range that doesn't apply once a package reaches 1.0.0. What is that rule, and why do many popular libraries intentionally stay below 1.0.0 for years despite being widely used in production?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Versions starting with 0 (like 0.4.2) are 'anything can change, anytime' under SemVer - even a MINOR bump can break things. Maintainers use this to keep freedom to redesign the API while people are still testing it out, even if lots of people already use it.

open as a page

You're responsible for a widely-consumed internal library published to hundreds of services inside a large engineering org. Design a policy that lets SemVer's MAJOR/MINOR/PATCH numbers actually stay trustworthy over years of contributions from dozens of different teams, given that no single person reviews every change. What mechanisms would you put in place, and what's the failure mode of skipping each one?

level: principalimportance: should knowfreq 35%

basics

~20 s

You need more than good intentions: a clear written definition of what's 'public API,' automated checks that catch accidental breaking changes before merge, a required human sign-off for anything flagged as breaking, and a changelog that's actually generated from real diffs. Skip any of these and the version number slowly stops meaning what it claims.

open as a page