skip to content

Under Semantic Versioning 2.0.0, what do the MAJOR, MINOR and PATCH fields mean, and how do you decide which one a given change bumps?

level: juniorimportance: must knowfreq 78%

answer

  1. a promise to consumers, not a counter
  2. three fields, three compatibility claims
  3. the public API defines breaking
  4. diff size is irrelevant
  5. 0.y.z promises nothing

basics

~20 s

Semantic Versioning encodes compatibility: bump MAJOR for a backward-incompatible change to the public API, MINOR for backward-compatible new functionality, PATCH for a backward-compatible bug fix. The public contract decides the bump, not how much code changed.

solid answer

~50 s

SemVer 2.0.0 formats a version as `MAJOR.MINOR.PATCH`, and each field is a promise to consumers. PATCH means a backward-compatible bug fix; MINOR means backward-compatible functionality was added (including marking something deprecated); MAJOR means something in the declared public API broke. Bumping a field resets the lower ones to zero, and a published version is immutable — you never re-release the same number with different content. Two special rules matter in practice. `0.y.z` means initial development, where the spec explicitly allows anything to change at any time; `1.0.0` is the moment you commit to a public API. Pre-releases hang off a hyphen (`2.0.0-rc.1`) and always sort *below* the release they precede, while build metadata after a plus sign is ignored for precedence entirely. The size of the diff is irrelevant: a one-character change to a default value can be a MAJOR.

code

json · 9 lines
json
{
  "name": "billing-client",
  "version": "1.4.7",
  "dependencies": {
    "http-lib": "^1.2.3",
    "parser-lib": "~1.2.3",
    "crypto-lib": "1.2.3"
  }
}

go deeper

for a junior

Recall the three fields and state plainly that MAJOR means breaking, MINOR means new-but-compatible, PATCH means a fix. Be ready to classify two or three concrete example changes on the spot.

for a middle

Explain how the bump is derived from the declared public API rather than the diff, and cover the 0.y.z rule, pre-release ordering, and how a caret or tilde range in a consumer's manifest turns your bump into their upgrade.

for a senior

Show the judgment: what your project declares as public surface, how you handle a bug fix that changes depended-on behaviour, and how you enforce the policy in CI with API-diff tooling instead of trusting authors to grade their own change.

for a principal

Own the compatibility policy across an organisation — what counts as breaking, how long each major line is supported, what a deprecation window costs consumers, and how you keep major bumps rare enough that teams actually adopt them.

## What a version number is actually for A version string is not a serial number; it is a machine-readable claim about compatibility aimed at whoever consumes the artifact. Semantic Versioning 2.0.0 (semver.org) makes that claim explicit so a dependency resolver, and not just a human, can decide whether an upgrade is safe. The format is `MAJOR.MINOR.PATCH`, where each field is a non-negative integer with no leading zeros. ## The three fields - **PATCH** — a backward-compatible bug fix. Callers that worked on the old version work unchanged on the new one. - **MINOR** — backward-compatible functionality was added. New methods, new optional parameters, new configuration keys. The spec also puts "marked something as deprecated" here: a deprecation is an announcement, not a removal. - **MAJOR** — a backward-incompatible change to the public API. Anything a reasonable consumer could be relying on that no longer behaves the same way. Bumping a field resets everything to its right to zero: after `1.4.7`, a MINOR release is `1.5.0`, not `1.5.7`. And a released version is immutable — if `1.4.7` shipped a broken artifact, the answer is `1.4.8`, never a re-cut `1.4.7`. ## You must declare what "public API" means SemVer is only meaningful once you say what the contract covers. For a library that is usually the exported types and functions, but it should also cover things consumers depend on in practice: - removing or renaming a public symbol → MAJOR - narrowing what an input accepts, or widening what is required → MAJOR - changing a default value, an error/exception type, or a serialization format → MAJOR - raising the minimum language runtime or platform requirement → MAJOR by most projects' policy - adding a new optional parameter with a default, so existing calls still compile → MINOR The amount of code touched is irrelevant. A one-line change to a default timeout is a MAJOR if callers' behaviour changes; a 5,000-line internal rewrite with an identical surface is a PATCH. ## The 0.y.z escape hatch Under `0.y.z` the spec says anything may change at any time — the public API is not considered stable. This is honest during early development and dishonest once real consumers exist, which is why projects that sit at `0.x` for years annoy their users. Tooling papers over it: npm's caret range treats `^0.2.3` as `>=0.2.3 <0.3.0`, effectively promoting the middle field to the major slot for zero-versions. ## Pre-releases and build metadata A hyphen introduces dot-separated pre-release identifiers; a plus sign introduces build metadata: ``` 2.0.0-alpha < 2.0.0-alpha.1 < 2.0.0-beta.2 < 2.0.0-rc.1 < 2.0.0 2.0.0+build.5 == 2.0.0+build.9 # same precedence ``` A pre-release always has *lower* precedence than the associated normal version, numeric identifiers compare numerically and alphanumeric ones compare as text, and build metadata is ignored when comparing. That last rule is the one that bites: you cannot use `+build.N` to distinguish two releases, because to a resolver they are the same version. ## Why consumers care Dependency ranges are written directly against these rules. In npm, `^1.2.3` accepts any `1.x` at or above `1.2.3`, while `~1.2.3` accepts only `1.2.x`; a correct MINOR bump therefore reaches every caret-pinned consumer automatically, and a mislabelled breaking change reaches them as a broken build. Some ecosystems encode the rule even harder — a Go module's major version 2 and above must appear in the import path itself (`/v2`), so a major bump is a different module. ## Where SemVer under-delivers SemVer is a social contract, not a mechanism. It cannot describe behavioural regressions — a PATCH that makes an operation ten times slower, or fixes a bug that consumers were depending on. Anything observable will eventually be depended on by somebody, so "breaking" is broader than the type signature suggests. Mature projects narrow the gap with an explicit written compatibility policy plus API-diff tooling in CI (japicmp for Java, cargo-semver-checks for Rust) that fails the build when the computed surface change outranks the version bump on the branch.

  • You need to fix a bug, but consumers have written code that depends on the buggy behaviour. Is the fix a PATCH or a MAJOR?
    Strictly, if fixing it changes observable behaviour that reasonable consumers rely on, it is a MAJOR — SemVer describes compatibility, not intent. In practice teams weigh how many callers depend on it, whether the old behaviour was documented, and whether it is a security fix. Common compromise: ship the fix as a MINOR behind an opt-in flag, then flip the default in the next MAJOR.
  • Why can't you use SemVer build metadata (the part after the plus sign) to distinguish two builds of the same version?
    Because the spec says build metadata is ignored when determining precedence. `1.4.0+abc` and `1.4.0+def` compare as equal, so a resolver has no way to prefer one and a range cannot select between them. If two artifacts must be distinguishable to consumers, they need different `MAJOR.MINOR.PATCH` or pre-release identifiers — build metadata is only an annotation for humans and provenance tooling.
  • When is a project genuinely ready to leave 0.y.z and cut 1.0.0?
    When you are willing to commit to the public API and to the cost of a MAJOR bump for anything that changes it. That means the surface is declared, deprecation and support policies exist, and there are real consumers whose builds you do not want to break. Staying at 0.x once consumers depend on you is a way of avoiding accountability, not a technical state.

saying these in an interview costs you the question

  • Bumping MAJOR because the change felt large
  • Treating a default-value change as a patch
  • Re-tagging a published version with new content
  • Assuming 0.x versions carry compatibility guarantees
  • Thinking a pre-release sorts above its release

context