← Writing

The store is part of the system

Backend deployment is a solved problem in the sense that it is your problem. You choose when, you choose how, and if it goes wrong you roll back. Nobody asks permission.

Mobile is not like that, and the difference is more structural than "there is a review step". The stores are participants in your architecture. They impose deadlines you did not agree to, they can refuse your release for reasons unrelated to your code, and the version people are running is a distribution rather than a fact.

One codebase, several apps

Our apps ship under more than one brand. Same code, different names, different icons, different colours, different store listings — because partners sell the product as their own.

That multiplies everything downstream of the build. Each brand is a separate store entry with its own metadata, screenshots, review cycle and release state. A single fix is not one release, it is one release per brand per platform, each of which can be at a different stage. My commit history is full of build entries naming two brands at once, precisely because keeping those in step was a thing that had to be actively done rather than something that happened.

And they can diverge involuntarily. One brand's submission clears review; the other is held for a question. Now the same code is live for some users and not others, and any server-side change has to account for both being in the wild simultaneously.

Rejection is often not about the code

One of my favourite commits from this period fixes the alpha channel in an app icon. Not a crash, not a policy violation, not a feature — a transparency channel in a PNG that the store will not accept.

That is representative. Submissions get bounced for metadata, for screenshots that no longer match the interface, for a privacy declaration that does not cover a library somebody added, for an entitlement you needed but did not justify. None of it is visible to a test suite, and all of it costs a review cycle.

The practical consequence is that the release checklist is not a build script. It is a list of things a human verifies, and treating it as automatable is how you discover the manual step by failing it.

The upgrade treadmill is not optional

Android requires apps to target a recent platform version to remain publishable, and that requirement moves every year with a hard deadline. This is not a suggestion, it is not driven by anything you want, and missing it means you cannot ship updates at all.

Each bump brings behavioural changes. Permissions that were granted become prompted-for. Background execution tightens. APIs you depend on are restricted. So the work is not "change a number in the build file", it is "change a number and then find out what broke".

My history has a sequence I would point any junior engineer at: support the new target version, then revert to the previous one, then do it properly later. That is not a failure. That is the correct response to discovering mid-flight that a mandatory upgrade has consequences you had not budgeted for — take the revert, ship the things that were waiting behind it, come back when you can do it deliberately.

The mistake would have been pushing through because the deadline is real. Deadlines set by somebody else are still real when you are halfway into a bad week.

Point releases break things

Another commit: updating a phone-number parsing library because the version we had did not work on a specific iOS point release.

Not a major version. A patch. Somewhere between two decimal places, behaviour changed enough to break a dependency used on every screen that displays a number — and the only way to find out was that it started happening to users on a version that did not exist when we shipped.

That is the part of mobile that has no backend equivalent. Your runtime updates itself, on hundreds of millions of devices, on a schedule you do not control, and it can break code you have not touched in a year. There is no staging environment for "next week's OS release on a device somebody actually owns".

Your users are on every version at once

The thing that most changes how you write code: there is no current version.

A backend deploy means everyone is on the new thing in minutes. A mobile release means a slow diffusion — some people update overnight, some in a month, some have automatic updates off and are running something from two years ago. All of them are talking to the same API.

So every client-server change is a compatibility exercise by default, and it is permanent. You cannot remove an endpoint because the last app version that used it shipped; you can remove it when the last user of that version has upgraded, which is a different and much later date, and one you have to actually measure.

It also means the app has to tell the server what it is capable of, rather than the server assuming. That is why our requests carry a version — not for analytics, but so the server can decide whether this client can cope with a newer response shape.

Releases are driven by crashes

Reading the history back, a striking number of releases exist to fix crashes. Build numbers appear in commit messages with reasons attached — a build to fix random resets caused by failing to read preferences, a build to fix chat crashes, a build with a potential fix for something intermittent.

That cadence is a consequence of everything above. A crash on the backend is a log entry and a hotfix within the hour. A crash on mobile is a percentage of your users unable to use the product until a fix clears review and they choose to update. The urgency is higher and the levers are slower, which is an uncomfortable combination and the main reason mobile teams care about crash-free rates in a way backend teams rarely care about equivalent metrics.

What I took from it

Treat the store as a dependency with an SLA you did not negotiate. It has deadlines, it has opinions about non-code artefacts, and it can block you for reasons unrelated to quality. Plan releases around it rather than being surprised by it.

Mandatory upgrades deserve their own slot. Platform target bumps are not maintenance to be squeezed in. They are dated, unavoidable work with unknown consequences, and the right response to finding out mid-flight is to revert and reschedule rather than push through.

Version negotiation belongs in the protocol. When you cannot make clients upgrade, the server has to be told what it is talking to. Building that in early is much cheaper than retrofitting it the first time you need to change a response shape.

Multiplying brands multiplies releases, not code. The engineering cost of white-labelling a mobile app is small. The operational cost — listings, reviews, screenshots, release states that drift apart — is not, and it is paid every single release rather than once.

Also in this series