PackagingCon

Versioning for User-Facing Changes vs API Breakages
2021-11-10 , Room I

Semantic Versioning (MAJOR.MINOR.PATCH) is a common approach to versioning
libraries that separates changes into fixes (PATCH), additions (MINOR), and
breakages (MAJOR). Though simple, SemVer has two primary limitations that can
make it difficult for developers to work with:

  1. User-facing changes, such as new features or redesigns, are not separated
    from API breakages. Therefore, the compatibility between versions is harder
    for maintainers to understand as the impact of MAJOR updates can vary
    significantly (ex. Python 1->2 vs 2->3). In consequence, some projects
    now use year-based versioning or 'ZeroVer' (where MAJOR is always 0),
    thus avoiding the question of API compatibility entirely.

  2. API breakages are always represented by the MAJOR version and do not take
    into account different types of breakages, such as source vs binary
    compatibility. Additionally, tooling can be used to repair many common types
    of breakages (such as renaming) which do not have significant impact on how
    the library is used.

The purpose of this talk is to raise awareness of these limitations, demonstrate
the use cases for having multiple levels of API versioning, and propose
alternative versioning methods that can incorporate different types of API
breakages.


Semantic Versioning (MAJOR.MINOR.PATCH) is a common approach to versioning
libraries that separates changes into fixes (PATCH), additions (MINOR), and
breakages (MAJOR). Though simple, SemVer has two primary limitations that can
make it difficult for developers to work with.

The first limitation is that user-facing changes (such as new features or
redesigns) are not separated from API breakages, which itself causes two issues:

  1. Compatibility between versions is harder for maintainers to understand as
    the impact of MAJOR updates can vary significantly (ex. Python 1->2 vs
    2->3). Some release may cause breaking changes even though the overall
    library works the same, while others may maintain backwards compatibility
    but offer new (ideally better) features (ex. Java 8, which added lambdas
    introducing new options for API design).

  2. Developers hesitate to make the 1.0.0 release (and other major releases)
    for reasons related to the above as well as the effort involved in getting
    key downstream dependencies to update to avoid compatibility issues. Some
    projects now use year-based versioning or 'ZeroVer' (where MAJOR is always
    0), thus avoiding the question of API compatibility entirely .

Second, not all API breakages are the same. The most common example of this is
binary breakages, where compiled output fails to with new versions but the
source itself remains compatible. Even at the source level, some types of
breakages like renaming are 'simple' and can be automatically repaired with the
appropriate tooling, while others can require major refactors. There are also
breakages specific to the type of application - as a surprising example,
Minecraft Forge (a Minecraft API for client-side mods) encourage mods to use
separate version for mod compatibility as certain types of changes may break
player's worlds (effectively, data versioning).

There are a few potential solutions to this, and unfortunately all of them make
versions just a bit more complicated. The most straightforward one is to
maintain two versions - a true semantic version for the project, and an API
compatibility version:

  • Project version: PROJECT.FEATURE.PATCH, which represents the high-level
    changes to the project as it evolves.
  • API compatibility version: PROJECT.SOURCE.BINARY, which represents the API
    breakages to the project. SOURCE and BINARY compatibilities work as
    expected, but there is an additional PROJECT level for changes that have a
    large impact to how the API can be used. This is currently heuristic, but the
    inclusion of automated migrations may be able to formalize the idea of
    'minor' breaking changes versus 'major' ones to provide strict validation.

In summary, software version needs to better account for the difference between
user-facing changes and API breakages, as well as account for different types
of breakages. One potential solution is to maintain a separate version strictly
for API compatibility, coined PROJECT.SOURCE.BINARY, to help maintainers
understand the potential impact of updating versions.

See also: Slide Deck (see Google Slides link in description for sources & additional readings in speaker notes) (1.3 MB)

I'm a graduate student in Computer Science at the University of Florida researching programming language design. I currently work on Rhovas, a programming language for API design and enforcement emphasizing software maintainability.

Ask Me Anything: WillBAnders@gmail.com