---
title: 'Version Management'
source: 'https://academia.sh/en/courses/cross-platform-production/version-management'
course: 'Cross-Platform Production Practice'
language: en
updated: '2026-10-21T05:02:51+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explains the difference between the version number and the build number and who manages each'
    - 'Shows how a fix, a feature, and a breaking change increase the number in semantic versioning'
    - "Builds the server contract the client asks for as a capability and measures the feature's operability"
    - 'Shows by measurement that the client version number alone does not determine operability'
---

# Version Management

Version management governs which build of an application ships under which number and how the client talks to the server. This lesson measures how well a feature works when it asks for a new contract from a server that offers only the old one.

The previous lesson referred to the build by a single build number and gave the number no meaning. In a real application a number does two jobs: it tells the user which version they are running, and it tells the team which build carries which defect. This lesson covers what the numbers are, who increments them, and how a version number relates to the server.

The question of the course stays the same: how well does this feature work under this condition? In this lesson the source of the condition is the server. The client asks for a contract, and the server either offers that contract or does not.

## Version Number and Build Number

A mobile application has two separate numbers, and the two are often confused.

The version number is the number a person reads. It appears on the store page, it is written on the settings screen, and it is what a user says when support asks which version they are on. The team decides the version number, because the number carries a meaning: how much this release changed compared with the previous one.

The build number is the number a machine reads. The store uses it to tell apart several builds uploaded with the same version number. The fix for a rejected build may keep the same version number, but the build number has to increase. The delivery pipeline described in the previous lesson increments this number; a build number managed by hand is one of the places where pipelines get stuck most often.

The two platforms keep these two numbers in different fields. The two fragments below show only where the fields are; the fragments are not measured.

```kotlin
// Platform code: Android version fields
android {
    defaultConfig {
        versionCode = 142
        versionName = "4.2.1"
    }
}
```

```yaml
# Platform code: iOS version fields
CFBundleShortVersionString: "4.2.1"
CFBundleVersion: "142"
```

On Android, `versionName` is the version shown to the user and `versionCode` is the integer the machine sorts by. On iOS, `CFBundleShortVersionString` carries the version shown to the user and `CFBundleVersion` carries the build number. In a framework that connects to native views through a bridge and in a framework that runs its own rendering engine alike, these two fields are written to the same place in the end, the platform's own configuration file; the framework only adds a layer that feeds those files.

## Semantic Versioning

Splitting the version number into three parts is a common scheme: major, minor, and patch. In the number `4.2.1`, the major version is four, the minor version is two, and the patch is one. Each part makes a promise.

- A patch fixes a defect without breaking existing behavior. A user who installs a patch does not need to learn anything new.
- A minor version adds a new capability without breaking existing behavior. The patch resets.
- A major version brings a change that may break existing behavior. The minor and the patch reset.

This scheme matters more on mobile than for a web service, because the old version keeps living on the user's device. The server is updated and reaches everyone at once; the client is not updated, and stays old until the user decides. The major version number is therefore not only a label; it is a signal to the team that is planning which clients the server still has to talk to.

Producing the number is mechanical and goes wrong when done by hand, so it is good to leave it to a small function. The `nextVersion` function in the lesson's helper file writes this rule.

## Release Notes

Every release ships with release notes. The notes address two readers: the user who reads the store page and the team that watches the release. The note for the user is short and focused on behavior, such as "the payment screen was redesigned". The note for the team says which changes entered the release, which of them shipped closed behind a flag, and which server version the release requires.

The last item is neglected and costs a great deal later. If a release requires a new server contract, the notes must say so plainly, because publishing the client before the server offers that contract makes the feature work incompletely and silently. The section below measures that shortfall.

## The Contract Between Client and Server

The client and the server talk through a contract: which fields exist, in which format, under which names. The contract also has a version, and a server can offer several versions at the same time. An old client asks for the old contract, and a new client asks for the new one.

With a breaking change, the server keeps offering the old contract for a while longer, because old clients are still in use. The opposite also happens: the new client may have been published while the server has not yet moved to the new contract. The client that enters the store and the server that goes live are not updated at the same moment; in the interval between them, the new client meets the old contract.

From the point of view of operability, the shape of the problem is clear once more. The version of the contract is a capability: `api:v2` or `api:v3`. If a feature in the client asks for the new contract and the server does not offer it, the feature runs with a missing capability. Sometimes this shortfall blocks the feature entirely; sometimes the feature shows less information using the old fields. The second is a shortfall that has a fallback.

## Measuring the Condition of the Contract

For the measurement, take the order history. `OrderHistory` asks for the new contract, the `api:v3` capability; that contract also returns the line-item details of each order. On a server that offers only `api:v2`, the screen opens but shows the orders with the old fields, without line-item details. This is a fallback and sits at the `degraded` level. The core is familiar from the first lesson and is given again in the same form in every lesson; the lesson does not touch it.

```javascript
// operability.mjs — classifies how fully a feature works under a condition
export const LEVELS = ["full", "degraded", "deferred", "disabled"];
const FALLBACKS = ["degraded", "deferred"];

export function feature(name, needs, fallback = {}) {
  return { name, needs, fallback };
}

export function condition(name, has) {
  return { name, has };
}

function judge(item, cond) {
  const missing = item.needs.filter((need) => !cond.has.includes(need)).sort();
  let level = "full";
  for (const need of missing) {
    const given = FALLBACKS.includes(item.fallback[need]) ? item.fallback[need] : "disabled";
    if (LEVELS.indexOf(given) > LEVELS.indexOf(level)) level = given;
  }
  return { level, feature: item.name, condition: cond.name, missing };
}

export function assess(features, conditions) {
  const records = [];
  for (const cond of conditions) {
    for (const item of features) records.push(judge(item, cond));
  }
  return records;
}

export function printLevels(records) {
  for (const r of records) {
    const missing = r.missing.length === 0 ? "-" : r.missing.join(",");
    console.log(`${r.level}: ${r.feature} @ ${r.condition} missing=${missing}`);
  }
}

export function countLevels(records) {
  for (const name of LEVELS) {
    console.log(`${name}: ${records.filter((r) => r.level === name).length}`);
  }
  console.log(`affected: ${records.filter((r) => r.level !== "full").length}`);
}
```

After the core comes the lesson's helper file. `conditionsFor(servers)` produces a condition from each server definition: the network is present on every server, while the contract capability comes from the version the server offers and is written with the `api:` prefix. `nextVersion(version, change)` increments a three-part number according to the kind of change. There are three features. `OrderHistory` needs the network and the `api:v3` capability; if `api:v3` is missing, it works in a degraded way. `Catalog` is the product list and `Profile` is the user profile; both need only the network.

```javascript
// versions.mjs — builds a condition from the contract a server offers, increments the version
import { feature, condition } from "./operability.mjs";

export function conditionsFor(servers) {
  return servers.map((server) => condition(server.name, ["network", `api:${server.api}`]));
}

export function nextVersion(version, change) {
  const [major, minor, patch] = version.split(".").map(Number);
  if (change === "major") return `${major + 1}.0.0`;
  if (change === "minor") return `${major}.${minor + 1}.0`;
  return `${major}.${minor}.${patch + 1}`;
}

export const historyFeatures = [
  feature("OrderHistory", ["network", "api:v3"], { "api:v3": "degraded" }),
  feature("Catalog", ["network"]),
  feature("Profile", ["network"]),
];
```

**RO3.** The model builds the contract the server offers as a single capability: a server offers either `v2` or `v3`. A real server can offer several versions together, and the client can decide which one to use through negotiation; the rule does not see this negotiation and looks only at the condition's `has` list. The model builds this operability's rule; the model is not the framework itself.

The first measurement puts two servers side by side for the same client. Three number increments are printed first; these are the lesson's local output. Then the six records follow.

```javascript
// main.mjs
import { assess, printLevels } from "./operability.mjs";
import { conditionsFor, nextVersion, historyFeatures } from "./versions.mjs";

let version = "4.2.0";
for (const change of ["patch", "minor", "major"]) {
  const next = nextVersion(version, change);
  console.log(`version: ${version} + ${change} = ${next}`);
  version = next;
}
const servers = [
  { name: "api-v2", api: "v2" },
  { name: "api-v3", api: "v3" },
];
printLevels(assess(historyFeatures, conditionsFor(servers)));
```

```text
version: 4.2.0 + patch = 4.2.1
version: 4.2.1 + minor = 4.3.0
version: 4.3.0 + major = 5.0.0
degraded: OrderHistory @ api-v2 missing=api:v3
full: Catalog @ api-v2 missing=-
full: Profile @ api-v2 missing=-
full: OrderHistory @ api-v3 missing=-
full: Catalog @ api-v3 missing=-
full: Profile @ api-v3 missing=-
```

The number increments show the version rule: a patch increases only the last part, a minor version resets the patch, and a major version resets both. In the records, `OrderHistory` is degraded only on the `api-v2` server; the missing capability is `api:v3`, and its fallback is defined as degraded. On the same server `Catalog` and `Profile` are full, because neither asks for a contract version. On the `api-v3` server all three features are full.

The word degraded says something concrete here: the order history opens and the orders are listed, but the line-item details are absent. Had the feature been disabled, the screen would not have opened at all. A defined fallback means the feature can offer something even with the missing capability.

## A Patch Upgrade Does Not Change the Capability

When a user installs a patch, the version number changes. The number changing does not mean that it brought in or took away one of the capabilities the feature needs. A patch fixes a defect and does not change the contract.

The second measurement compares two client versions against the same server. The condition names include the version, because two conditions cannot share a name; the contract offered is the same in both.

```javascript
// main.mjs
import { assess, printLevels } from "./operability.mjs";
import { conditionsFor, historyFeatures } from "./versions.mjs";

const installs = [
  { name: "4.2.0-api-v2", api: "v2" },
  { name: "4.2.1-api-v2", api: "v2" },
];
const records = assess(historyFeatures, conditionsFor(installs));
printLevels(records);

const key = (list) => list.map((r) => `${r.level}:${r.feature}:${r.missing.join(",")}`).join("|");
const before = key(records.filter((r) => r.condition === "4.2.0-api-v2"));
const after = key(records.filter((r) => r.condition === "4.2.1-api-v2"));
console.log(`does a patch change the record: ${before === after ? "no" : "yes"}`);
```

```text
degraded: OrderHistory @ 4.2.0-api-v2 missing=api:v3
full: Catalog @ 4.2.0-api-v2 missing=-
full: Profile @ 4.2.0-api-v2 missing=-
degraded: OrderHistory @ 4.2.1-api-v2 missing=api:v3
full: Catalog @ 4.2.1-api-v2 missing=-
full: Profile @ 4.2.1-api-v2 missing=-
does a patch change the record: no
```

The two client versions give the same three results across six records, and the last line says so. The comparison leaves out the condition name, because the name is different anyway; what is compared is the levels, the features, and the missing capabilities.

**RO4.** The model assumes that a patch does not change capabilities, and this assumption rests on a promise: a patch does not break behavior. In reality, a patch can disable a fallback path or remove a capability by mistake. In that case the patch fixes one defect while introducing another, and the version number does not say so. The rule does not catch this case; what catches it is rebuilding the condition and measuring again after the patch.

## The Bounding Measurement

The focus record of this lesson is `OrderHistory @ api-v2`. The bounding measurement shows two things together. First, the focus record is degraded on the `api-v2` server and full on the `api-v3` server, and only one of six records is affected. Second, the client's version number does not enter the record: a client whose major version has been raised gives the same record on the same old server.

```javascript
// main.mjs
import { assess, printLevels, countLevels } from "./operability.mjs";
import { conditionsFor, historyFeatures } from "./versions.mjs";

const servers = [
  { name: "api-v2", api: "v2" },
  { name: "api-v3", api: "v3" },
];
const records = assess(historyFeatures, conditionsFor(servers));
printLevels(records.filter((r) => r.feature === "OrderHistory"));
countLevels(records);

const key = (list) => list.map((r) => `${r.level}:${r.feature}:${r.missing.join(",")}`).join("|");
const oldClient = assess(historyFeatures, conditionsFor([{ name: "4.2.0-api-v2", api: "v2" }]));
const newClient = assess(historyFeatures, conditionsFor([{ name: "5.0.0-api-v2", api: "v2" }]));
console.log(`client version does not enter the record: ${key(oldClient) === key(newClient) ? "yes" : "no"}`);
```

```text
degraded: OrderHistory @ api-v2 missing=api:v3
full: OrderHistory @ api-v3 missing=-
full: 5
degraded: 1
deferred: 0
disabled: 0
affected: 1
client version does not enter the record: yes
```

The first two lines carry the lesson's operability: the same feature, the same code; only the contract the server offers changed, and the record moved from full to degraded. The count lines say that of six records, five are full and one is degraded; affected is one.

The last line shows the limit of the model. The version number is a label; what the feature needs is not the version number but the contract the server offers. A client numbered `5.0.0` is as degraded on a server that does not offer `api:v3` as a client numbered `4.2.0`. The number is therefore a hint but not proof: the team writes in the release notes which client asks for which contract, while operability is determined by the contract the server actually offers.

This distinction also determines the release order. If a client that asks for the new contract is published before the server moves to that contract, some of the users see the feature degraded. Publishing the server first and the client afterward closes that interval.

## Summary

- This lesson's operability: degraded. `OrderHistory @ api-v2` came out degraded because the server offered only the old contract and the fallback for the `api:v3` capability is degraded; on the same server `Catalog` and `Profile` stayed full.
- The team decides the version number and the user can see it; the pipeline increments the build number, and it has to be new on every upload.
- In semantic versioning a patch fixes a defect, a minor version adds a capability and resets the patch, and a major version brings a breaking change and resets both.
- Of six records, five were full and one was degraded; affected was one. A patch upgrade did not change the record on the same server.
- A client with a raised major version gave the same record on the old server; the contract the server offers, not the version number, determines operability.

## Next Step

In this lesson the server was a fixed condition and the client was a single build. The next lesson, `Staged Rollout and Rollback`, covers the case in which a new version reaches only a portion of the users. It compares halting the rollout, rolling back, and rolling forward with a fix, and it measures the condition of each bucket separately.
