Lesson 21 of 26
Versioning and Breaking Changes
How a shared component library ships breaking changes without breaking every consumer at once — semantic versioning, deprecation windows, and codemods.
A component used by one consumer can change however its author likes. A component published as a shared, versioned package — even an internal one consumed only by other teams in the same company — makes a promise every time it ships a new version, and breaking that promise without warning is one of the fastest ways to make engineers stop trusting a design system enough to keep using it.
What the version number is supposed to communicate
Semantic versioning (major.minor.patch) exists specifically so a version
number alone tells a consumer how much risk is involved in adopting it, without
reading a changelog first: a patch bump is meant to be safe to take
automatically — bug fixes, no API change. A minor bump adds new,
backward-compatible capability — safe to adopt, nothing existing breaks. A
major bump signals that something may break — adopt deliberately, read
the changelog, expect to change call sites.
This only works as a system if it's actually honored. Shipping a breaking change as a patch or minor version — even a small one, even one that "almost nobody" will hit — breaks the entire premise that consumers can trust automatic patch/minor updates, which is precisely the convenience semantic versioning is meant to provide. Once one breaking patch burns a team, they stop trusting patch bumps generally, and start manually reviewing every update — which is strictly worse for everyone than the discipline of honoring the version contract in the first place.
Deprecation windows, not instant removal
- Step 1
A prop or component is marked deprecated
Released in a minor version, with a visible warning (console warning, TypeScript @deprecated tag) — nothing breaks yet.
- Step 2
Consumers see the warning in their own builds
Well ahead of any actual removal, so migration can be scheduled rather than forced.
- Step 3
A documented migration path is provided
What to replace the deprecated API with, ideally alongside a codemod that automates the mechanical rewrite.
- Step 4
The deprecated API is actually removed
In a later major version — after enough time has passed for consumers to have realistically migrated.
This window matters precisely because a shared component has many consumers on many different schedules — some teams can migrate immediately, some are mid-sprint on something unrelated and need weeks. Removing something the same release it's deprecated denies every team that flexibility, forcing whoever hasn't already migrated to do so immediately, out of their own schedule, or pin to an old version and start drifting from the library.
Codemods: automating the actual migration
For a widely-used component, "here's the new API, please update your call sites" can mean real, tedious, mechanical work repeated across dozens or hundreds of usages, often across several separate codebases. A codemod — a script that finds the old usage pattern and programmatically rewrites it to the new one — turns much of that migration into "run this command and review the diff," substantially lowering the actual burden a breaking change places on every consuming team, and meaningfully increasing how quickly teams actually do migrate rather than putting it off indefinitely.
Why this discipline is worth the overhead
None of this — careful version bumps, deprecation windows, codemods — is free to produce; it's genuinely more work than just changing the API and telling people in a Slack message. But a shared component library's entire value proposition depends on consumers trusting it enough to actually depend on it, and to take updates without fear. A library with a reputation for surprise breakage gets pinned to old versions, forked locally, or quietly abandoned in favor of teams rolling their own — all outcomes that undo the whole point of having a shared library in the first place.
What to remember
- Semantic versioning is a promise, not just a numbering convention — shipping a breaking change as a patch or minor version breaks that promise and erodes trust in every future version bump.
- A deprecation window (warn now, remove later, in a major version) turns a breaking change from a surprise into a schedulable migration for every consuming team.
- Codemods automate the mechanical part of migrating call sites, substantially lowering the real cost a breaking change imposes on consumers.
- A shared library's value depends on consumers trusting its versioning discipline — breaking that trust once has lasting costs beyond the one incident.
Check yourself
3 questions · pass 3/3 to unlock Theming and Design Tokens
1.A design system publishes a change that removes a previously-supported prop from a Button component, released as a patch version bump. What's the direct consequence for consumers?
2.Why does a deprecation warning period (marking something deprecated in a release, then actually removing it in a later major version) matter for a widely-used shared component?
3.What's the practical purpose of a codemod shipped alongside a breaking API change in a shared component library?
3 left to answer