Back to publishing & release Automate semantic versioning Manage release channels Harden the supply chain

Choosing Fixed vs Independent Versioning in a Monorepo

A monorepo that publishes several packages has to decide how their version numbers relate. In fixed (or "locked") mode every package shares one version: release 4.2.0 bumps all of them to 4.2.0, even packages that did not change. In independent mode each package has its own version and is bumped only when it changes. The choice affects what consumers see, how many releases you publish, how internal dependency ranges are written, and how much release tooling you need. It is also hard to reverse without confusing consumers, so it deserves a deliberate decision rather than a tool default. This guide compares the two modes, shows the middle ground of linked groups, and configures each in Changesets and Lerna.

What each mode looks like to consumers

Consider three packages — @acme/core, @acme/react and @acme/vue — where a release changes only @acme/react:

One release under fixed, linked and independent versioning Shows the resulting versions of core, react and vue after a change to react only, under fixed, linked and independent modes. @acme/core @acme/react (changed) @acme/vue Before 3.4.0 3.4.0 3.4.0 Fixed 3.5.0 (no changes) 3.5.0 3.5.0 (no changes) Linked (all three) 3.4.0 3.5.0 3.4.0 Independent 3.4.0 3.4.1 or 3.5.0 3.4.0
Fixed mode republishes unchanged packages; independent mode publishes only what changed; linked mode aligns only the packages that do change.

Fixed tells consumers "these packages are one product; install matching versions". Angular, Babel's core packages in the past, and many design systems use it. Independent tells consumers "each package evolves on its own"; utility collections and tool ecosystems typically use it, because their packages are adopted one at a time and rarely all together. The release mechanics are covered in Semantic Versioning and Release Automation.

Choosing between them

Picking a versioning mode A decision chain based on whether packages must be installed at matching versions, whether they are released as one product, and how often they change together. Must consumers use matching versions? Fixed e.g. a framework's core + renderer yes Marketed and documented as one product? Fixed or linked one changelog, one version story yes no Do packages usually change together? Linked groups align versions only among changed packages yes no Independent each package on its own semver line no
Fixed suits packages that are only valid together; independent suits packages consumed separately.

Arguments for fixed:

  • Consumers never wonder which versions are compatible — the same number is always right.
  • One changelog and one version story for documentation and support.
  • Internal dependencies can be exact pins, because everything is released together.

Arguments for independent:

  • Versions carry meaning: a major bump on @acme/vue means Vue users need to act, and nobody else does.
  • No meaningless releases of unchanged packages, which also avoids dependency-update noise for consumers.
  • Scales to repositories with dozens of loosely related packages.

Neither mode is more correct in general; each encodes a different promise to consumers, and the right choice is the one whose promise matches how your packages are actually used. The hidden cost of fixed mode is semver dilution: a breaking change in one package forces a major for all of them, so consumers of unaffected packages see majors with no breaking changes and learn to ignore them. The hidden cost of independent mode is compatibility complexity: consumers mixing @acme/react@5 with @acme/core@3 need clear peer ranges to know what works.

How each mode affects consumers' dependency trees

The mode you choose shows up in every consumer's node_modules, not just in your changelog. Picture an application that depends on @acme/react and a third-party plugin that depends on @acme/core.

Deduplication of a shared internal dependency An application depends on acme react and a plugin; both depend on acme core. With caret ranges one core copy satisfies both; with exact pins two copies may be installed. consumer app @acme/react 5.2.0 core: ^5.1.0 or 5.2.0 third-party plugin core: ^5.0.0 @acme/core one copy if ranges overlap
Caret ranges let the package manager install one @acme/core; exact pins from fixed mode can force two.

With independent versioning and caret ranges, @acme/react@5.2.0 accepts any @acme/core@^5.1.0, so the package manager installs a single core that satisfies both it and the plugin. With fixed versioning and exact pins, @acme/react@5.2.0 requires exactly @acme/core@5.2.0; if the plugin's lockfile resolved 5.1.3, the consumer can end up with two copies. For libraries with module-level state — registries, caches, context objects — two copies are a real bug, not just wasted bytes.

Fixed-mode ecosystems handle this by asking consumers to upgrade all packages together and by documenting that rule prominently; some also declare the core as a peer dependency of the other packages rather than a regular dependency, which forces a single copy and makes version mismatches visible as peer warnings. If you choose fixed mode for published packages, decide explicitly which of those two strategies you follow.

Changelogs and communication

The two modes also produce different release notes. Fixed mode naturally yields one release entry per version covering every package, which reads well as a product changelog. Independent mode yields one changelog per package; consumers of a single package see only what matters to them, but anyone tracking the whole ecosystem must read several files. Changesets generates per-package CHANGELOG.md files in both modes; many fixed-mode projects add a combined release summary on their documentation site or GitHub releases so the product story stays readable.

Configuring Changesets

Changesets defaults to independent versioning. Fixed and linked behaviour is configured in .changeset/config.json:

{
  "$schema": "https://unpkg.com/@changesets/config@3/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "fixed": [["@acme/core", "@acme/react", "@acme/vue"]],
  "linked": [],
  "ignore": ["@acme/docs"]
}
  • fixed: each inner array is a group whose packages always share a version and are always released together, changed or not.
  • linked: each inner array is a group whose changed packages are bumped to the same version; unchanged packages in the group are left alone.
  • updateInternalDependencies: when a dependency is bumped, dependents are bumped too — patch (always) or minor (only when the dependency's range would no longer be satisfied).

Workflows for Changesets are in Automating Releases with Changesets.

Configuring Lerna

{
  "$schema": "node_modules/lerna/schemas/lerna-schema.json",
  "version": "3.4.0",
  "npmClient": "pnpm"
}

A concrete version number means fixed mode. Use "version": "independent" for independent mode. Lerna has no linked mode; teams emulate it with separate release runs.

Internal dependency ranges

The mode should match how packages depend on each other through the workspace protocol:

Mode Internal range Published as
Fixed workspace:* exact version, e.g. 3.5.0
Independent workspace:^ caret range, e.g. ^3.4.0
Linked workspace:^ caret range

Exact pins in fixed mode are correct because packages are only guaranteed to work at matching versions. Caret ranges in independent mode let consumers deduplicate when two of your packages depend on different compatible versions of a third.

Peer dependencies between your own packages deserve their own thought. A plugin package that peers on @acme/core should declare the range of core versions it truly supports ("^3.0.0"), which in independent mode may be wider than the version it was last tested with.

Switching modes

Moving from fixed to independent is straightforward: change the configuration, and from the next release each package moves on its own from its current version. Moving from independent to fixed requires choosing a starting version at least as high as the highest current package version, bumping every package to it in one release, and explaining the jump in the changelog. Either way, announce the change, because consumers who relied on "matching versions" in fixed mode need to know that numbers no longer line up, and update any documentation that tells users to install matching versions.

Worked example: a design system that outgrew fixed mode

A design system started as three fixed-mode packages: tokens, React components and icons. Two years later it had fourteen packages, and each weekly release published fourteen new versions — mostly unchanged icon and token packages. Consumers complained about update noise, and a breaking change in the date picker forced a major for icons. The team kept tokens and components in a fixed group (they are only valid together), moved icons and the eleven utility packages to independent versioning, and switched their internal ranges to workspace:^. Weekly releases now publish two to four packages, and majors only appear where something actually broke.

Prevention and guardrails

  • Decide the mode before the first release from the monorepo, and document it in the contributing guide.
  • Match internal ranges to the mode — exact for fixed, caret for independent.
  • Use linked groups when packages change together often but not always.
  • Dry-run releases after changing modes to confirm the planned versions.

Frequently Asked Questions

Can fixed and independent packages coexist in one repository? Yes. In Changesets, packages in a fixed group share versions while all other packages are independent.

Does fixed mode require publishing unchanged packages? Yes — that is the definition. Every package in the group gets the new version, so consumers can always install matching versions.

How do consumers know which independent versions are compatible? Through peer dependency ranges and changelogs. Make peer ranges accurate and test combinations you claim to support.

What about semantic-release in a monorepo? semantic-release is designed around one package per repository. Community plugins add monorepo support, usually in independent mode driven by commit scopes. For fixed or linked groups, Changesets or Lerna are simpler choices.

Do prereleases follow the same mode? Yes. In fixed mode every package in the group gets the same prerelease version (for example, 4.0.0-beta.1); in independent mode only packages with changes enter prerelease. Changesets' pre mode respects fixed and linked configuration.

Is there a cost to changing mode later? Mainly communication. Moving to independent is technically simple; moving to fixed requires a version jump. Either way, consumers who relied on the old convention need clear release notes.

Related

Semantic Versioning and Release Automation