Understanding Semantic Versioning
MAJOR.MINOR.PATCH looks simple, but the rules behind it are what let dependency managers upgrade packages safely and automatically.
Semantic Versioning (SemVer) defines version numbers as
MAJOR.MINOR.PATCH, where each position has a precise, binding
meaning rather than being an arbitrary release counter.
The three numbers
- MAJOR — incremented for incompatible API changes. Upgrading across a major version may break existing code.
- MINOR — incremented for new, backwards-compatible functionality. Existing code keeps working after the upgrade.
- PATCH — incremented for backwards-compatible bug fixes only. No new functionality, no breaking changes.
So 2.4.1 to 2.5.0 means new features were added but nothing
should break; 2.4.1 to 3.0.0 means something incompatible
changed and upgrading needs review.
Why dependency managers rely on this
Tools like npm, Composer, and Cargo let you constrain a dependency
to a range, e.g. ^2.4.1 (in npm/Composer terms, "compatible with
2.4.1, meaning any 2.x.x version that's >= 2.4.1"). This only works
safely because SemVer makes a binding promise: if a package author
follows it correctly, any 2.x.x release is guaranteed not to break
code written against 2.4.1. That promise is exactly what lets
npm install or composer update pull in newer patch and minor
versions automatically without manual review, while still refusing
to silently cross a major version boundary.
Pre-1.0.0 and pre-release versions
Versions below 1.0.0 are considered initial development, where
anything may change at any time — there's no compatibility promise
yet. Pre-release versions append a hyphen suffix, like
2.0.0-beta.1, and are considered lower precedence than the
associated normal release (2.0.0-beta.1 < 2.0.0).
The catch
SemVer is a convention, not something a package manager can enforce
automatically — it depends entirely on the package author correctly
judging whether a change is actually breaking. A patch release that
accidentally breaks something is a real, if hopefully rare, failure
mode, which is exactly why lockfiles (package-lock.json,
composer.lock) exist: to pin the exact versions that were tested
together, rather than trusting version ranges alone in production.