Semantic Versioning in Practice: What MAJOR.MINOR.PATCH Actually Promises
A version number is a contract about breakage. When to bump each part, how 0.x, pre-release tags and build metadata work, and where SemVer falls short.
On this page
A version number is not decoration. It is a sentence to everyone who depends on you: “upgrading is safe”, “upgrading adds things”, or “upgrading may break you”. Semantic versioning is the shared grammar for that sentence.
The three numbers
MAJOR . MINOR . PATCH
2 . 4 . 1
| Part | Raise it when | Consumer reaction |
|---|---|---|
| PATCH | You fix a bug without changing the public API | Upgrade freely |
| MINOR | You add functionality, backward compatible | Upgrade, optionally use the new feature |
| MAJOR | You make an incompatible change to the public API | Read the notes, plan the migration |
The rule that surprises people: when a higher part goes up, every lower part resets to zero. After 1.4.7, a breaking change is 2.0.0.
A release timeline
- 0.1.0: initial development
Everything may change at any time. Nobody should depend on stability yet.
- 1.0.0: the first stable release
You are declaring a public API and promising to evolve it by the rules below. Reaching 1.0.0 is a decision, not a date.
- 1.0.1: a bug fix
Same behaviour, defect corrected. Patch.
- 1.1.0: a new backward-compatible feature
A new endpoint, option or function. Existing callers are untouched. Minor.
- 2.0.0: an incompatible change
A removed field, a changed return type, a renamed function. Existing callers can break. Major.
Pre-releases and build metadata
A hyphen introduces a pre-release, a plus sign introduces build metadata.
1.0.0-alpha < 1.0.0-beta < 1.0.0-rc.1 < 1.0.0
1.0.0+20130313144700 (build metadata: ignored when ordering)
A pre-release sorts before the release it precedes, so release candidates are never mistaken for the final version. Build metadata records provenance, such as a build timestamp or commit, and two versions that differ only there are considered equal in precedence.
SemVer only means something once you have declared the public surface: HTTP endpoints, exported functions, CLI flags, config keys, database migrations that consumers rely on. Changing anything outside that surface is not a breaking change, however much it hurts someone who relied on it anyway. Write the surface down.
Classifying a change honestly
Most arguments about version bumps are arguments about classification.
| Change | Bump | Why |
|---|---|---|
| Fix a crash in an existing function | PATCH | Behaviour now matches the documentation |
| Add an optional parameter with a default | MINOR | Existing calls still work |
| Add a new response field | MINOR | Tolerant clients ignore it, but strict ones may break: document it |
| Make a previously optional parameter required | MAJOR | Existing calls now fail |
| Remove or rename a public function | MAJOR | Existing callers break |
| Tighten validation so previously accepted input is rejected | MAJOR | Behaviour contract changed |
| Deprecate something but keep it working | MINOR | Still compatible, but announce removal |
Do
Deprecate in a MINOR release with a clear removal version, keep a changelog that names every breaking change, and run an API diff in CI so a breaking change cannot ship as a patch by accident.
Don't
Ship a breaking fix as a patch because “it was a bug anyway”. Someone’s build depended on the old behaviour, and the version number just told them it was safe.
Where SemVer falls short
- It trusts the author’s judgment about what is breaking. There is no enforcement in the spec.
- Large projects bump MAJOR so rarely, or so often, that the number stops carrying signal. Some projects prefer calendar versioning, which tells you age instead of risk.
- It says nothing about how big a MAJOR change is. 2.0.0 might rename one function or rewrite the architecture.
- Dependency ranges (
^1.2.0,~1.2.0) rely entirely on authors getting it right, which is why lockfiles exist.
Takeaway
Use SemVer as a promise you can keep: declare your public API, classify every change against it, reset lower numbers on a bump, and let a changelog and an automated diff keep you honest. The number is cheap to write and expensive to get wrong, because someone else’s build is trusting it.
Frequently asked questions
What do MAJOR, MINOR and PATCH mean in semantic versioning?
MAJOR increases for incompatible API changes, MINOR for backward-compatible new functionality, and PATCH for backward-compatible bug fixes. When a higher part increases, the lower parts reset to zero, so 1.4.7 followed by a breaking change becomes 2.0.0, not 2.4.7.
What does a version below 1.0.0 mean?
Versions starting with 0 are for initial development, where anything may change at any time and the public API should not be considered stable. Releasing 1.0.0 is the declaration that you have a public API and will follow the rules for changing it.
How are pre-release and build metadata versions ordered?
A pre-release such as 1.0.0-rc.1 sorts lower than 1.0.0, and pre-release identifiers compare in order. Build metadata after a plus sign, as in 1.0.0+20130313, is ignored when determining precedence, so two versions differing only in build metadata are equal.
Is SemVer enough to prevent breaking changes?
No. It only works if authors correctly classify their changes, and what counts as breaking is a judgment about the public API. Compatibility tests, API diffing tools and changelogs catch the cases where a maintainer calls a breaking change a patch.
/* Comments */
Comments are offline right now — we reconnect automatically, nothing is lost.