major minor patch — when each applies (semver in practice)
As engineers, we build, maintain, and consume software. Whether it's a small internal library, a critical microservice, or a public API, understanding how versions work is fundamental to smooth development cycles and happy users. Semantic Versioning (SemVer) isn't just a convention; it's a contract. It's a promise to your users about what they can expect when they upgrade your software.
Ignoring SemVer, or applying it inconsistently, leads to "dependency hell," broken builds, frustrated developers, and a general loss of trust. In this article, we'll dive deep into the MAJOR.MINOR.PATCH structure, explore when to increment each part, and discuss practical considerations and common pitfalls.
The Core of SemVer: MAJOR.MINOR.PATCH
At its heart, SemVer defines a three-component version number: X.Y.Z, where:
- X is MAJOR: Incremented when you make incompatible API changes. This means users of your software will likely need to change their code to upgrade.
- Y is MINOR: Incremented when you add new functionality in a backwards-compatible manner. Existing users can upgrade without changing their code, but they gain access to new features.
- Z is PATCH: Incremented when you make backwards-compatible bug fixes. These are typically internal changes that don't affect the public API or add new features.
The critical concept here is backwards compatibility. This is the line in the sand that dictates whether you bump a PATCH, MINOR, or MAJOR version.
When to Increment PATCH (0.0.x)
A patch release signifies a maintenance update. These are the "safe" upgrades that your users should feel comfortable applying without fear of breaking anything.
What it means: * Bug fixes: Fixing an error in existing functionality. * Performance improvements: Optimizing existing code without changing its external behavior. * Internal refactoring: Code cleanup, readability improvements, or minor architectural tweaks that don't touch the public API. * Security fixes: Addressing vulnerabilities without changing the API.
Key rule: Absolutely no API changes, no new features. If a user upgrades from 1.2.3 to 1.2.4, their existing code should continue to work exactly as it did before, only better (e.g., fewer bugs, faster).
Examples:
* You find a typo in a log message for an error condition.
* An internal database query is inefficient, causing slow responses. You optimize it.
* A specific edge case in a utility function (calculateDiscount) was returning an incorrect value; you fix the calculation.
* A UI component had a visual glitch in a specific browser; you adjust the CSS.
Pitfall: Accidentally introducing a breaking change in a patch release. This erodes trust. Be meticulous with testing and code reviews. If you fix a bug but that fix subtly changes the expected behavior of an edge case that someone might have been relying on (even if it was technically a bug), you might be crossing into minor or even major territory. When in doubt, err on the side of caution.
When to Increment MINOR (0.x.0)
A minor release is about growth. You're adding value and expanding the capabilities of your software without disrupting existing integrations.
What it means: * New features: Introducing entirely new functionality. * Extending existing APIs: Adding new optional parameters to a function, new fields to a data structure, or new endpoints to an API that don't break existing calls. * Deprecating functionality: Marking existing features as deprecated (to be removed in a future major release), but still supporting them in the current version.
Key rule: Consumers of your library or API should be able to upgrade from 1.2.x to 1.3.0 (or 1.3.x) without modifying their existing code. Their old code should continue to work, and they now have new options available if they choose to use them.
Examples:
* You add a new parseDate function to your date-utils library, which previously only had formatDate.
* A REST API endpoint /users now accepts an optional include_profile=true query parameter to fetch additional data.
* You add a new onSuccess callback option to a UI component that previously only had an onError callback.
* You introduce a new sendEmail method to a messaging service, alongside the existing sendSMS method.
Pitfall: Misjudging backwards compatibility. For instance, changing an optional parameter to a required one, or adding a new required field to a request body, constitutes a breaking change and should trigger a MAJOR bump. Always ask: "Will any existing user's code break if they simply update the version number?" If the answer is yes, it's not a minor release.
When to Increment MAJOR (x.0.0)
A major release is a big deal. It signals that significant, incompatible changes have occurred, requiring users to actively migrate their code. These releases are often accompanied by extensive migration guides.
What it means: * Incompatible API changes: Modifying existing function signatures, changing data structures, removing features. * Significant architectural overhauls that break existing integrations. * Upgrading a major dependency that itself introduces breaking changes and forces you to expose those breaks to your users.
Key rule: Users must modify their code to upgrade from 1.x.x to 2.0.0. This is the point where you break the contract of backwards compatibility.
Examples:
* You change the signature of a core function processOrder(orderId, items) to processOrder({ orderId, customerId, lineItems }) to improve clarity and add new requirements.
* A REST API previously returned user IDs as integers, but now returns them as UUID strings for consistency.
* You remove a feature that was deprecated in a previous minor release.
* Your library moves from a callback-based asynchronous API to a Promise-based or async/await-based one.
Pitfall: Reluctance to make major changes when needed. Sometimes, technical debt accumulates, or a better design emerges. Holding back on a necessary major version bump can lead to an unmaintainable codebase. Conversely, making major changes too often can frustrate users and lead them to seek alternatives. Plan major releases carefully, bundle breaking changes, and provide clear migration paths.
The "0.y.z" Problem
SemVer has a special rule for initial development: "Major version zero (0.y.z) is for initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable."
This means that during 0.y.z, even a minor bump (0.1.0 to 0.2.0) can introduce breaking changes. Many projects treat 0.y.z as "anything goes" until 1.0.0 is released, at which point strict SemVer rules apply. If you're building a new library, 0.1.0 is a common starting point, with 0.2.0, 0.3.0, etc., used for any significant changes before you declare it stable with 1.0.0.
Practical Considerations & Edge Cases
- Pre-release versions: SemVer allows for pre-release identifiers like
1.0.0-alpha.1,1.0.0-beta.2, or1.0.0-rc.3. These signify that the version is not yet stable and may not meet the compatibility requirements of the main version. They are useful for testing and early feedback. - Build metadata: You can add build metadata using a plus sign, e.g.,
1.0.0+20231027.hash. This information is ignored when determining version precedence. - Internal vs. External APIs: SemVer primarily applies to your *public