Continuous Delivery Through App Store Review
Store review is the one place a third party sits between your merge and your users. It is a fixed cost in the batch-size equation, not a wall, and the monthly cadence it seems to force is a choice.
Mobile is the one place in modern software delivery where somebody else owns your release path. Between the moment a change is merged and the moment a user can run it sits a review queue belonging to a company you do not work for, operating to criteria you did not write, on a timetable you cannot see. And once a binary is out you cannot take it back. There is no rollback, only a new submission, which goes to the back of the same queue.
This is a genuine constraint, not an excuse. The people who tell you to just deploy more often have usually never shipped a binary.
What is not real is the conclusion most organisations draw from it. The reasoning goes: review is unpredictable, therefore releases are expensive, therefore we release monthly, therefore every feature waits for the train, therefore mobile is the slowest-moving part of the product organisation and everyone has quietly accepted that. Each step follows from the last. The first premise is true. The conclusion is a choice, made by default rather than deliberately.
The move that breaks the chain is the same one that breaks it everywhere else: separate the thing that is expensive from the thing that is valuable. Shipping a binary is expensive and gated by a third party. Revealing a feature is cheap and entirely within your control. Teams that conflate the two inherit the store's cadence as their product cadence. Teams that separate them ship binaries on a boring fixed rhythm nobody thinks about, and reveal features continuously, to any percentage of users they choose.
Review is a transaction cost, not a wall
The batch-size economics apply to mobile without modification. A release has a fixed cost independent of how much is in it: store metadata and screenshots, regression across a device matrix, a submission, a wait of uncertain length, and the possibility of a rejection that restarts the wait. Against that sits the holding cost of finished work sitting in a branch or behind a flag, learning nothing from real users.
What makes mobile feel different is that one component of the transaction cost — the queue time — is genuinely outside your control. So teams treat the whole transaction cost as fixed and settle at a large batch size.
That is the error. The queue is one term in a sum, and everything else in that sum is yours: how long the regression pack takes, how much manual device testing is required, how the release branch is cut and stabilised, how metadata is produced, how the build is signed and uploaded, how much human coordination the release needs. In most mobile organisations the parts you control are substantially larger than the part you do not, and they are the parts nobody has attacked, because the unfixable part gets all the attention.
There is a second asymmetry that matters more. Queue time is a waiting cost, not an effort cost: it consumes calendar, not capacity, and waiting runs in parallel with other work in a way effort cannot. If you drive your controllable release effort close to zero, you can submit far more often than you do now, because each submission barely costs you anything to make.
The release train that actually works
The quarterly release train is an antipattern for good structural reasons. But there is a version of the release train that is correct, and mobile is its natural home. The difference is one rule.
A bad release train waits. The date approaches, a feature is nearly ready, somebody asks for two more days, and the train slips. Once it can slip, the cutoff is a negotiation, and every cutoff becomes a political event in which the loudest stakeholder wins two days and everyone's quality suffers.
A good release train leaves. Pick a cadence — weekly is achievable for most products, fortnightly is comfortable, monthly is the outer bound of what should count as fast. Cut the branch at a fixed time. Submit whatever is on trunk and green. If a feature is not finished, it travels on the next train, which is close enough that nobody needs to fight about it.
The rule that makes this work is that the train never waits for a feature, because no feature is ever visible on arrival. Everything ships dark. The binary contains half-built features, finished but unannounced features, and features that will never be turned on. None of that matters, because none of it is reachable. The train carries capability, not commitments.
This inverts the emotional logic of the cutoff. On a waiting train, missing it is a disaster, so half-finished work gets forced in at the worst possible moment. On a leaving train, missing it costs nothing, because the reveal was going to be a flag flip days after the binary landed anyway.
| Property | Waiting train | Leaving train |
|---|---|---|
| Cutoff behaviour | Slips when features are late | Fixed, never moves |
| Cost of missing | A full cycle of delay | Days, and usually nothing |
| Incoming quality | Worst code arrives last, least reviewed | Trunk is always releasable |
| Stabilisation | Multi-week hardening phase | Short, or none |
| Feature reveal | Coupled to binary release | Independent, server-controlled |
Once the train leaves reliably, stakeholders stop asking "can you get this into the release?" and start asking "when are we turning it on?" — a product question with a product answer, which nobody needs an engineer's permission to settle.
Decoupling the binary from the feature
The mechanism is server-controlled configuration, and the discipline around it matters more than the technology. The detail is treated separately; here is what it buys at the release-process level.
Ship dark by default. New code paths land in the binary disabled, with the compiled-in default off. A user with no network, or whose config fetch failed, gets the old behaviour. This is what makes a fixed-cadence train safe: shipping incomplete work carries no user-facing risk.
Reveal on a server decision. Turning a feature on is a config change, effective within one fetch cycle, requiring no submission and no review. The reveal can be to a percentage, a cohort, a region, a build range or an internal group, and it is reversible the same way it was made.
Keep a kill switch on anything that talks to a backend. Client code that depends on a service should be disableable independently of whether the feature is "on", so that when the service degrades you can stop the client hammering it without shipping.
The net effect is that your deployment batch shrinks to whatever the train carries, while your release batch — the changes a user experiences at once — shrinks to one. That second number governs diagnosis cost, and you have taken control of it.
What you genuinely cannot decouple
Being honest about the limits is what makes the rest of this credible.
The native runtime surface. Anything requiring new native code — a system permission you have not previously declared, a new capability, an SDK that ships compiled code — cannot be flag-flipped in. The flag can gate whether you use it. It cannot put it there.
Anything the platform vets at submission. Store metadata, declared permissions and privacy disclosures, age ratings and the app's stated purpose are evaluated against the binary you submitted. Using a flag to reveal behaviour that materially differs from what was reviewed invites trouble, and the trouble is disproportionate because the remedy sits with the reviewer. Platform rules here change and are enforced with judgement; read current vendor guidance rather than any summary, including this one.
Bug fixes in code that is already out. The one that hurts. If a crash sits in an unconditionally executed path, no server flag saves you. The mitigations are architectural and must exist beforehand: flags around risky paths, a kill switch, server-side compensation, and a staged rollout that limited how many users saw the bad build at all. All are investments made before the incident, which is why they never get made during one.
Minimum platform versions, build tooling and dependency floors. Properties of the artefact, changed only by shipping.
The existence of old versions. Every binary you have released is still running on somebody's device, possibly for years — a permanent condition with its own consequences.
Shortening everything either side of the gate
You cannot compress the queue. You can compress everything around it, and that is where the available time is.
Before submission — make the branch a non-event. If cutting a release branch triggers a week of stabilisation, the problem is not the store, it is that trunk was never releasable. Trunk-based development is the precondition for a fixed-cadence train, not an optional refinement. Cutting a branch should be a tag, and the branch should be a candidate immediately.
Before submission — automate the device matrix. Manual regression across devices is usually the largest controllable cost in a mobile release, and the one most often defended as irreducible. It rarely is. A device farm running a smoke and critical-path suite across a representative spread of hardware and operating system versions, on every build rather than every release, moves the cost to the commit path, where it gates nothing.
Before submission — generate the release artefacts, do not assemble them. Release notes, screenshots, signing, versioning and the upload itself should be one pipeline invocation. A weekend of work that pays back for years, deferred indefinitely because each step is only mildly annoying.
Submit before you are finished. The train carries dark code, so the binary need not be feature-complete to be worth submitting. Submitting earlier overlaps the queue with work you were doing anyway rather than putting it at the end — a reframe that often recovers more calendar than any tooling change.
After approval — control the rollout. Staged rollout is the mobile substitute for rollback. Release to a small percentage, watch release-health signals, expand or halt. A halted rollout at a small percentage is a contained incident; a full release with a crash in it is a week of your life.
After approval — separate approval from reveal. Approval is not a launch. The approved binary is capability arriving quietly. The launch is a flag change, made when marketing, support and product are ready, with your rollback being a second flag change.
Where the constraint is actually binding
Run the exercise from flow metrics on your last ten changes, recording when the work started, when it merged, when it entered a submitted binary, when that binary was approved, and when a user could see the behaviour. That gives four intervals.
In almost every mobile organisation that has not deliberately attacked this, merge to submitted-binary is the largest and entirely self-inflicted: the change sat waiting for a cycle that comes round monthly. Review is typically the smallest of the four. Approval to user-visible is often large too, and also self-inflicted, because rollouts get paused for reasons nobody is tracking. The constraint everyone complains about is usually the one contributing least — which is not an argument that it does not exist, but that it is being used as cover.
What to do on Monday
Measure the four intervals above for your last ten user-visible changes, from records rather than memory. Put the four medians on one slide. This is the argument, and it will make itself.
Pick the cadence you will hold and write down the rule that the train leaves without waiting. Weekly if you can, fortnightly if you cannot yet. Announce the dates for the next quarter so the cutoff stops being negotiable.
Then ship the next feature dark: merge it to trunk behind a default-off flag, let it travel on the normal train, and reveal it by config days after the binary is approved. Do it once, deliberately. Afterwards the team has direct evidence that binary release and feature release are different events, and evidence is harder to argue with than a diagram about batch size.