Ahh, the humble app version. While the act of versioning an app might seem pretty inconsequential, if you take a step back you see just how often version numbers are relied upon in the development and release lifecycle. Your entire team – engineering, product, QA, CX, and more – regularly need to refer to specific app versions in their day-to-day lives as they prepare work for new releases, track stability and health across old releases, coordinate testing efforts, and communicate changes and provide support to end users.
So, perhaps app versioning is worth some thought! Versioning software has been a thing since forever and, whether you’re an engineer, a PM, or another mobile-adjacent character, you’ve definitely dealt with it firsthand at some point. But have you stopped to ask “why?” Different teams approach versioning differently and, at first glance, it might seem like there are no particular standards. There's a tendency to approach things on a gut-feeling basis: "This release contains some pretty big changes, so it’s a 2.0!" or "We’re only shipping a few small things, so this is version 1.9.1!". Is everyone just doing whatever feels right or looks the coolest? Have you ever wondered if there's a “correct" way to approach versioning?
Like everything in software engineering, there's actually a concrete set of considerations and standards to follow that will help you to decide how to version your apps and get the most out of versioning for your team.
I'm inclined to say that you should standardize everything in life that can be standardized effortlessly – as in, the standardization doesn’t make things overly complicated and/or annoying. But of course there are some specific, practical arguments for why you should follow a general pattern for your app’s versioning.
As I touched on above, app versions are handled by a wide variety of people on your team, and these people are very often having to communicate versions back and forth amongst themselves. Sane, standardized versioning makes these comms easier and more error-proof. This is especially the case during release cycles, when test binaries are passed around and more stakeholders who might be less technical are looped into proceedings. For example, a member of the CX team dogfooding a pre-prod version of your app might refer to the version string which includes the build number when reporting an issue they experienced. Or, an engineer might add a version string to a Jira ticket for a feature they want quickly verified by the product team. In either case, having a standardized way to identify versions (and builds) helps streamline the development workflow.
Standardized versioning is great for triaging and debugging issues because it uniquely identifies specific builds in a way that implies certain things about their contents, and makes them traceable. You can get immediate answers about the "state" of the product in a troublesome version relative to the latest, such as what might've introduced an issue, how long ago the version was introduced, if this is the most recent version, and so on. For example, a bug reported in a build with version 2.5.0 (where the previous version was 2.4.0) hints at a regression introduced as a part of a minor version release. When combined with proper version control tagging hygiene, it’s possible to easily inspect the changes made between versions 2.4.0 and 2.5.0 to more quickly identify the source of the bug.
Standardizing your versioning also helps make your software predictable. For public projects and libraries, this is incredibly important as it gives end users (in this case, other software engineering teams) an immediate expectation of what an update is changing in terms of code and compatibility. Also, because most dependency managers are built with standardized versioning in mind, end users can more flexibly – but still predictably! – constrain included versions of software (e.g. to some “known safe” range of future versions).
In the case of closed-source code, like mobile apps, standards have less to do with predictability around the contents of code and more to do with predictability around kinds of features being shipped and the timing of updates – both for your team and for end-users. This is just as important though, and the importance grows proportionally with the size of your team as an increasingly bigger group of people need to easily determine the contents of a release and/or when certain features will land.
Now that we’ve considered some of the ways standardizing your versioning can benefit your team and end users, let’s look at the most common approaches for actually accomplishing that standardization.
Semantic Versioning, or SemVer, is one of the classic approaches to versioning, and probably still the most popular at the moment. Its strong foundation is rooted in preventing “dependency hell,” where breaking changes are hard to track and you're otherwise unable to easily and safely move your project forward.
According to SemVer, you should version your product with three decimal-delimited digits (e.g: 1.0.3), following the pattern <code><major>.<minor>.<patch><code>. When it comes to incrementing each digit, you increment the:
SemVer is the versioning scheme that most directly speaks to the code predictability angle I mentioned above. Depending how end users set up their dependencies, they can include versions of your software flexibly but with confidence about what sort of version they’ll be getting.
While SemVer is perhaps most geared towards libraries, it can also be useful for end-user products like mobile apps. The difference is that you need to replace the concept of versioning around dependency-related, "API changes" with something that is more relevant to your end-users. Generally this ends up being a mapping of major, minor, and patch versions to different kinds of feature work. For example, one way to use SemVer for mobile apps is to apply it as such:
Calendar Versioning (CalVer) is a variation of SemVer that focuses on time instead of content. It expands on the idea that SemVer’s fundamental motivation (signaling and safeguarding dependencies) doesn't apply as much to end-user facing products. Instead, CalVer calls for a versioning convention based simply on the passage of time, versus some subjective mapping of arbitrary numbers.
Exactly how CalVer is implemented can vary. CalVer versions could reflect literal dates of releases (e.g. 2022.4.26 for a release on the 26th of April, 2022), or they could simply be some value that increments with time (e.g. the nth release ever, or even build numbers). Most commonly and popularly in the mobile world, you see a combination of the two: the first part of the version anchors to a high-level calendar date – for example, the current year (2022.X.X for some release in 2022) and perhaps month as well (2022.4.X for some release during April, 2022) – and the second part of the version simply counts up (e.g. 2022.5.0 for the fifth release of 2022, or 2022.4.3 for the third release of April, 2022). Again, the important thing here is that the version indicates passage of time, instead of trying to tie back to some code or feature-level change.
CalVer is a popular versioning option for mobile teams operating at scale (whether in terms of team size or release frequency/continuity). When a team gets large, with many distributed product teams contributing work to each and every release, SemVer's basic "bump based on new features" approach is rendered a bit useless because every single release contains significant new features. And, for teams that ship continuously by decoupling feature work from releases (e.g. teams that operate release trains or use feature flagging extensively), SemVer loses meaning altogether and CalVer becomes a much more sensible way to track releases.
For mobile teams, there are a couple of specific CalVer schemas that are most common. One is the hybrid sort of versioning mentioned above which mixes a macro date reference (year) with a simple incrementing count of how many releases have shipped that year. Another popular approach, perhaps the most fundamentalist version of CalVer, simply increments the version every day, or with each build or release. Some real world examples:
The mobile use case is unique in that you need to think about “versions within versions.” In a given release cycle, you’re often creating multiple release candidate builds, and although end-users don’t care about these release candidates, they’re pretty important to your team. You need some way to account for each of these “sub-versions” individually, but also within the context of the macro cycle, so that you and your team can all be on the same page about exactly what is being tested and released.
Thankfully, both SemVer and CalVer allow for the addition of “extensions” (or “modifiers”) which are appended to the ends of versions. These allow you to differentiate between multiple internal or beta versions within a given “macro” release cycle. Here are some popular examples of version extensions:
Appending the build number to the version is probably the most common extension. It allows you and your team to easily differentiate and communicate about multiple builds created within a given release cycle. For teams running daily nightly builds, for example, this ability can be especially helpful when handling bug reports for issues that might've already been solved in a more recent build. An example of a build number as extension would be 18.104.22.168 (the 104th build of release 1.0.4).
Using the commit hash from which a release was built as an extension (e.g. 1.0.4.ba907ac) is another choice that can simplify debugging. When an issue is found and you need to check out that particular build's code, to debug what went wrong, all you need to do is copy the hash and fetch it in git. On the other hand, the commit hash gives you no additional information about the version, for example its recency, making it a less attractive choice when compared to the build number.
Using flags (such as alpha, beta, pre-release, etc.) as extensions is another fairly common practice. Unlike the other choices, although it helps qualitatively signal the relative maturity or stability of a given build, it doesn’t convey any explicit info that could make your life easier.
As I described earlier, for products like libraries that are pulled in via dependency managers, I'd argue that you should always use SemVer and version based on code changes, for API compatibility and predictability.
For a user-facing product like a mobile app, the decision is less clear-cut, but an underlying principle is that you should choose a versioning system that works best for your team. Considering your end-users and versioning for an external audience used to be more important, in that we all used to manually download updates for our apps. But now, in the age of more mature app stores and automatic updates, versioning has become an invisible thing from the point of view of most end-users. As such, app versioning considerations and benefits have shifted entirely in the direction of improving the well-being of the development team and the operation of its release cycles.
So, I recommend that you aim for something that feels comfortable and makes sense for your team, especially when it comes to how you approach your dev and release cycles.
Try to think about what you want to capture and convey to the team with your versioning:
Hopefully, we've covered how even something as trivial-seeming as app versioning can be leveraged to improve your team's development lifecycle and foster better communication with your teammates and even your users — if you take a moment to plan around it with intention. Do you have other tips or tricks to share on app versioning? Drop me a line!