Semver was designed for software that ships, gets adopted, and gets superseded on a one-to-three-year cycle.
Embedded SDKs ship into products with multi-decade lifetimes, recombine across multiple independently versioned components, and span a public surface that goes well beyond the C API. Three extensions to standard semver are what make the difference.
The semantic versioning specification at semver.org defines a clean compatibility contract: PATCH increments are bug fixes, MINOR increments are backward-compatible additions, MAJOR increments are breaking changes. The contract assumes a software artifact with a single public API surface, a single release stream, and a customer base that adopts the latest version more often than not. Adopt it, increment the numbers correctly, and customers can reason about upgrade safety from the version number alone.
For most software, that contract is sufficient. For embedded SDKs, it is necessary but not sufficient — and the gap between “necessary” and “sufficient” is where most version-management failures in embedded SDKs originate.
The reason is that embedded SDKs violate three of the assumptions that pure semver was designed for. The public surface is wider than the C API. The SDK is rarely a single component — it is a composition of independently versioned pieces that have to be validated together. And the products built on the SDK have production lifetimes measured in years or decades, not release cycles, which means the SDK has to maintain compatible support windows that extend far beyond the version it has currently shipped.
This post walks through the three extensions to standard semver that close the gap: applying the contract to the full public surface, defining a Long-Term Support policy that bounds the multi-decade liability, and managing multi-component composition through an explicit compatibility matrix. Each extension is mechanical to apply once it is named; the cost of not applying them compounds quietly until it surfaces as customer churn.
Extension 1: the contract covers more than the C API
Standard semver, as written, focuses on the C API — function signatures, struct layouts, header paths. For an embedded SDK, the public surface is wider than that, and a complete versioning contract has to cover all of it.
A representative mapping of change categories to required version increments looks like this:
- C API: function signature changed. MAJOR.
- C API: function removed. MAJOR.
- C API: new function added. MINOR.
- ABI: struct layout changed. MAJOR — every object file built against the old layout breaks.
- Kconfig symbol renamed. MAJOR — every prj.conf referencing the old name breaks.
- Kconfig symbol default changed. MINOR or MAJOR depending on impact — a stack-size default that pushes customers over their RAM budget is, in effect, a major-level change.
- CMake target renamed. MAJOR — every CMakeLists.txt referencing the old name breaks.
- Device tree binding property renamed. MAJOR — every overlay referencing the old name breaks.
- Device tree binding property added (optional). MINOR.
- Implementation changed, API unchanged. MINOR or PATCH depending on behaviour change.
- Bug fix with behaviour change. PATCH plus an explicit changelog note — even a fix that corrects a defective behaviour is a behaviour change for customers who had compensated for the defect.
The point of the table is not the categorisation in any individual row. The point is that the categorisation has to be applied uniformly across the entire public surface, not selectively to the C API alone. An SDK that applies semver rigorously to its C headers but treats build-system names as renameable in patch releases is shipping breaking changes without acknowledging them, and the contract that the version number is supposed to communicate is silently broken.
The discipline this enables is meaningful: a customer who knows that a patch release is genuinely safe across the full public surface will adopt patch releases promptly. That is exactly what the SDK team wants for security fix uptake. A customer who has been burned by a “patch” release that broke their build will hold every future release for an extra cycle of testing — which is exactly what the SDK team does not want.

Extension 2: Long-Term Support is part of the version policy
Embedded devices have multi-year, sometimes multi-decade, production lifetimes. A product shipped on the SDK in 2026 may need security patches in 2031, long after the mainstream SDK has moved through several major versions and accumulated breaking changes. Standard semver has nothing to say about this. It assumes that customers track the latest release and that older releases age out of relevance. In embedded, the older releases are exactly the ones in the field.
The extension that closes the gap is a formal Long-Term Support policy. The components of a credible LTS policy are concrete:
A designated LTS release per year, or per major silicon generation, identified at the time of release. The “latest” branch and the “LTS” branches are explicitly different artifacts with different upgrade implications, and customers choose which one to pin to based on their product’s lifecycle.
A committed support window — three years from the LTS designation date is a defensible minimum for industrial and consumer IoT, and longer windows are appropriate for medical or infrastructure markets. The window is published at LTS designation and updated on every PATCH release.
A backporting discipline limited to security fixes and critical bug fixes only. LTS releases receive no new features and no API changes; only PATCH-level increments are applied. This is the property that lets customers adopt LTS PATCH releases without re-validating their entire integration.
A separate Git branch — sdk-lts-3.x or similar — with a dedicated CI pipeline. The LTS branch is not a tag on the main branch; it is a maintained line of development with its own validation, its own release cadence, and its own changelog.
What this policy converts is an open-ended liability into a bounded, costed commitment. Without an LTS policy, customers are quietly absorbing the risk that their SDK version will become unsupported at an unspecified point in the future. With an LTS policy, customers can plan their security-patching strategy against a published end-of-life date, and the SDK team can size the engineering investment required to honour it. The cost is real — running parallel CI for an LTS branch is a measurable line item — but it is finite, and it is visible.
The vendors who have done this work have turned a customer-side anxiety into a sales advantage. The vendors who haven’t are quietly losing renewals they never see lost.
Extension 3: the compatibility matrix as a first-class artifact
The third extension addresses a problem that standard semver does not contemplate at all: an SDK is rarely a single component. It is a composition of independently developed pieces — the kernel, the HAL, the BLE stack, the Wi-Fi driver, the bootloader, TF-M, the OTA pipeline, the protocol clients — and each of them has its own version. The question “is this SDK release compatible with my application” is therefore not a single-version comparison; it is a query against a graph of validated combinations.
There are two models for managing this composition, each with its own trade-offs.
Lockstep versioning. All components carry the same version number as the SDK release. A release of SDK v3.2.1 means every component in the workspace manifest is tagged v3.2.1, regardless of whether each individual component changed in that release. This model is simple to communicate and guarantees that the validated combination is unambiguous: customers know exactly what set of component versions they are getting from a single SDK version number. The cost is that a bug fix in a single driver requires a coordinated release of every component, even those that have not changed. Lockstep versioning is the right answer for SDKs in their first three years, when component interaction surfaces are still being defined and the cost of inter-version incompatibility is high.
Independent component versioning. Each component carries its own semantic version, evolving on its own cadence. The workspace manifest or component registry lockfile records the validated combination. This model enables faster iteration on individual components and lets community contributors release on their own schedule, but it requires a dedicated compatibility matrix mapping which component version combinations have been validated together. A compatibility matrix that is not maintained automatically by CI will drift and become unreliable within a few releases. Independent versioning is appropriate for mature SDK ecosystems with stable component interfaces and automated compatibility testing.
In either model, the compatibility matrix is a first-class artifact, and ideally it is published in a machine-readable format so customer tooling can consume it programmatically:
# compatibility-matrix.yaml
sdk_version: “3.2.1”
validated_combinations:
– hal_version: “3.2.x”
rtos_version: “freertos-10.6.x”
ble_stack_version: “5.4.x”
tfm_version: “2.1.x”
mcuboot_version: “2.1.x”
validated_targets: [target-a, target-b, target-c]
lts: false
The discipline that keeps the matrix accurate is mechanical, not editorial. A CI job that attempts to build and test every defined combination against a representative target board set is the only mechanism that survives team rotation, schedule pressure, and the gravitational pull of the next silicon launch. Manually maintained matrices are obsolete within two release cycles, and the SDK team is left publishing a document that customers learn not to trust.
The matrix is also where version skew becomes manageable. When a developer upgrades only part of the SDK — for example, picking up a new BLE stack release without updating the HAL — version skew introduces interface mismatches that may compile cleanly but fail at runtime, because the C ABI is source-compatible without being binary-compatible. The compatibility matrix is the artifact that tells the customer whether the combination they are about to ship has been validated, or whether they are now in territory the SDK team has not tested.

Deprecation discipline is what makes the rest work
The three extensions above are the structural ones, but they all depend on a fourth practice that is procedural rather than structural: a deprecation policy that lets the SDK evolve without breaking customers in PATCH or MINOR releases.
A symbol slated for removal must be marked with a deprecation annotation — a Doxygen @deprecated tag and a compiler __attribute__((deprecated)) marker — in the MINOR release that introduces its replacement. The deprecated symbol must then be retained for at least one full LTS release cycle before removal in a subsequent MAJOR release. Every deprecation carries a changelog entry, a migration guide section pointing to the replacement API, and a timeline specifying the earliest MAJOR release in which removal may occur.
The deprecation message itself is part of the contract. A [DEPRECATED] sdk_uart_legacy_init() is deprecated. Use sdk_uart_init() instead. Will be removed in SDK v4.0. printed at compile time is the difference between a customer noticing the rename in advance and a customer being surprised by the removal three releases later. Silent deprecation does not work, because customers do not read changelogs as carefully as SDK teams hope.
What this policy enables is a release rhythm in which the SDK can evolve continuously without the version number lying. Renames, refactors, and replacements are introduced in MINOR releases as additive deprecations; the breaking removal is deferred to the next MAJOR release; customers see the deprecation warning on their next PATCH or MINOR adoption and can plan the migration on their own schedule. The version number stays honest, because the breaking change really is gated to the MAJOR boundary.
Why this matters more for embedded than for other software
The standard objection to extending semver this far is that it is process overhead — that the discipline costs more than it saves. For consumer web software that ships every two weeks and where customers automatically pick up updates, that objection has some merit.
For embedded SDKs, it does not. The customer base is not auto-updating. Each SDK adoption is a deliberate decision by an engineering team that has to weigh upgrade risk against bug-fix benefit, and the input to that decision is the version number plus the changelog. A version number that does not faithfully signal compatibility forces every customer to do their own integration testing on every release, which is what they were trying to avoid by using a versioned SDK in the first place.
The extensions described in this post — uniform application of the semver contract across the full public surface, a formal LTS policy with a published support window, and a CI-validated compatibility matrix for multi-component composition — are what turn a version number into a contract a customer can actually rely on. The cost is mostly process: release-time diff tooling, an LTS branch with its own CI lane, an automated compatibility-matrix job. The benefit is that the SDK ecosystem can evolve over a multi-decade horizon without forcing customers to absorb the integration cost of every release. That is the property that distinguishes an SDK that compounds in value over successive product generations from one that becomes a customer-side maintenance burden three years in.
Semver is necessary. The three extensions are what make it sufficient.

Shipping an SDK into products with a ten-year lifetime means your version numbers have to mean something.
needCode designs SDK versioning strategies — LTS policies, compatibility matrices, deprecation pipelines — that give customers a contract they can actually rely on across product generations. If you’re revisiting your SDK’s versioning or release architecture, let’s talk.
Book a free discovery call or get in touch
Further reading
- Monolithic, Meta-Tool, or Registry — the SDK delivery model that the compatibility matrix and component versioning described in this post has to be built on top of
- Documentation as a Product — the changelog and migration guide discipline that makes the versioning contract visible to developers
- Anatomy of a Production OTA Pipeline — one of the independently versioned SDK components that has to appear in the compatibility matrix
- Opaque Handles, Vtables, and Device Trees — the HAL API surface whose breaking changes drive the MAJOR version increments described in this post

