Semantic versioning: compatibility, prereleases and dependency ranges
Use semantic versions to communicate API compatibility. Work through breaking changes, prerelease ordering, dependency ranges and the limits of version labels in deployment.
TL;DR: Semantic versioning makes a promise about a declared public API. After 1.0.0, a compatible bug fix increments patch, compatible new functionality increments minor, and an incompatible API change increments major. A version label still needs evidence from tests and a precise artifact identity.
Declare the contract before choosing a number
Consider a client library at 2.4.1. Its public API includes exported functions, accepted inputs, returned values and documented errors. A fix that makes a documented valid input work can be a patch. Adding an optional argument with compatible behavior can be a minor release. Removing a supported argument requires a major release under the declared contract.
The same reasoning applies to a Terraform module's inputs and outputs, a Helm chart's documented values, or an internal HTTP API. First name the consumers and the promises they rely on. A team that cannot explain its public API cannot consistently choose a semantic version.
The SemVer specification defines the numbering and precedence rules. It treats 0.y.z as initial development, where the public API should not be considered stable. Do not infer a mature compatibility commitment merely because a dependency uses three numbers.
Make the change classification explicit
| Proposed change to a stable library | Expected increment | Reason to inspect the contract |
|---|---|---|
| Correct a result for a documented supported input | Patch | Some consumers may have depended on the defect |
| Add an optional function with unchanged existing behavior | Minor | New transitive requirements can still affect consumers |
| Remove an exported function | Major | Existing callers stop working |
| Change a response field from string to integer | Major | Schema and deserialization expectations change |
| Rewrite internals without changing the public contract | Depends on observable result | Performance and resource promises may be documented too |
A bug fix can disrupt real users even when classified correctly. Announce relevant changes and test representative consumers. Semantic versioning provides a vocabulary for the commitment; it cannot discover every undocumented dependency in a customer's application.
Prereleases are ordered candidates
A team preparing 3.0.0 might publish 3.0.0-rc.1, then 3.0.0-rc.2, followed by the stable release. A prerelease has lower precedence than the corresponding stable version. Build metadata after + does not affect precedence, so 3.0.0+build.12 and 3.0.0+build.13 have equal SemVer precedence even if their bytes differ.
The diagram assumes an established API after 1.0.0. It deliberately includes consumer validation before publication. An author assigning a number is making a compatibility claim that tests and release review should support.
A dependency range delegates an update decision
Suppose an application allows a library version range rather than an exact version. The resolver may select a newer compatible-looking version during a future installation. The meaning of a range operator belongs to the package manager, so consult that ecosystem's documentation before translating ^, ~ or wildcard syntax into a policy.
For example, npm's package documentation permits version ranges in dependencies. A lockfile captures a concrete resolved dependency graph for reproducible installation workflows. Commit and review changes to that graph. An unchanged application source tree can behave differently if a fresh resolution selects different dependencies.
For production, associate the human version with a source revision, dependency lock data and the built artifact digest. Promote that artifact. Rebuilding 2.4.2 separately for production adds another opportunity for inputs to differ, even though the release label stays unchanged.
Apply the idea to an infrastructure module
Assume a module publishes subnet_ids as a list and accepts instance_count with a default of two. Changing the output to a map breaks callers that index the list. Removing the default forces callers to provide an argument. Both require an explicit compatibility decision.
Adding an optional tag input can be compatible if its default preserves existing behavior. Changing that default later may alter production resources during an otherwise routine upgrade. Release notes should explain the plan impact, and upgrade validation should include a representative saved plan. A minor version promise about the module interface does not guarantee an apply will make no infrastructure changes.
Keep compatibility separate from rollback feasibility. A major release can be safe to deploy after a migration, while reverting it may fail if data has been converted irreversibly. Study rollback compatibility before treating a lower version number as a recovery plan.
Check the release decision
You change a library's timeout default from 30 seconds to 2 seconds, leave every function signature unchanged, and propose a patch. Is checking signatures enough?
No. The documented behavior and supported use cases are part of the contract. A two-second timeout may break supported slow operations. Determine whether the timeout was promised, test representative consumers and choose an appropriate version and migration path. If the old behavior must remain available, an explicit option can make migration controllable.
Practice with semantic versioning in a release pipeline. In an interview, explain the contract you are versioning before listing major, minor and patch.