Skip to content

Mobile API Versioning with a BFF: Shipping When You Can't Roll Back the Client

A mobile binary can't be rolled back and old versions linger, so safety and speed move server-side: a BFF, consumer-driven contracts, and backward-compatible versioning.

Ayhan Sipahi Ayhan Sipahi

On the server, a bad deploy is reversible in seconds; on mobile, a shipped binary is permanent, app-store review sits outside your control, and old client versions stay in use for months. That asymmetry is the design constraint that should drive your release strategy: the client is the one part of the system you cannot roll back, so both safety and speed have to move to the side you can change. The levers that follow from that constraint are a backend-for-frontend seam, consumer-driven contracts for independent deployability, backward-compatible API evolution, and a clear line for where server-driven UI earns its keep.

The Asymmetric Clock#

Two clocks govern a mobile product, and they run at different speeds. The server clock is fast and reversible: a regression caught in production is a redeploy or a rollback away, measured in seconds to minutes. The client clock is slow and one-directional. Once a binary is in users’ hands, you cannot recall it, and on iOS users cannot even downgrade to a known-good earlier build. The fix has to travel through app-store review and then wait for users to choose to install it.

Review time is bounded, but you do not control it. Apple states that, on average, 90% of submissions are reviewed in less than 24 hours, with a tail that can stretch to multiple days. That average is useful for planning; it is too variable to promise to stakeholders as a release date. Adoption is the slower half of the problem. Uptake of a new release is gradual and voluntary; even months after a major mobile OS ships, it reaches only about two-thirds of its install base, and the same shape applies to app updates. The practical takeaway is directional: old clients linger for weeks to months, and a meaningful slice of traffic always comes from versions you released long ago.

Client clock (one-way, slow)

Submit build

Review: 90% under 24h, tail of days

Voluntary adoption over weeks to months

Old versions never fully gone

Server clock (reversible, fast)

Deploy

Bug found

Rollback in seconds

Because the client clock cannot be sped up or reversed, the safety levers have to live server-side. Minimum-version gating and remote kill switches are common practice for exactly this reason: they let you disable a broken feature or force an upgrade for the oldest clients without waiting on review. Treat forced-update as a tool you can usually reach for; the store may not always honor it.

The BFF Seam#

The first structural lever is a backend-for-frontend (BFF): one backend per user experience, owned by the same team that owns the UI. Sam Newman describes the pattern as a backend dedicated to a single experience that simplifies the process of lining up the release of both the client and server components. The guideline is “one experience, one BFF”: a single shared gateway does not serve every consumer. The pattern was coined by Phil Calçado’s team at SoundCloud, and the API gateway catalog frames the same idea as a separate API gateway for each kind of client.

The BFF matters here because it is the seam where server-side change becomes possible. When the app talks to a backend the UI team controls, decisions that would otherwise be frozen in the binary, such as which fields to show, how to shape a response, which downstream services to call, and how to degrade when one is unavailable, move to a surface you can redeploy in seconds. The same team owning both sides also removes a coordination handoff: one team lines up its own client release and server change as a single deploy sequence.

Mobile app (frozen binary)

BFF (owned by UI team)

Service A

Service B

Service C

A BFF is another deployable to run, monitor, and secure, and the “one per experience” rule means an iOS, Android, and web product may carry several. The trade-off is worth it when the UI team would otherwise be blocked on backend teams for every change, or when each experience needs a meaningfully different response shape. It is harder to justify when one thin client talks to a single stable service, where a BFF adds latency and on-call burden with little to absorb, and only earns its place when the client clock would otherwise dictate your release pace.

Contracts That Let You Ship Alone#

A BFF lets the UI team move, but it sits in front of downstream services owned by other teams, and the app sits in front of the BFF. Independent deployability across those boundaries is a contract problem. The failure mode is the big-bang integration: teams hold their changes until a coordinated release window, deploy a pre-tested set of applications together, discover incompatibilities only when everything meets, and lose the release speed a BFF was meant to provide.

Consumer-driven contracts invert the dependency. Each consumer publishes the subset of provider behavior it actually relies on; the provider verifies against the union of those expectations. The Pact documentation captures the key property: any provider behavior not used by current consumers is free to change without breaking tests. Breaking changes surface pre-merge, in CI, against the real expectations of real consumers, well before a shared staging environment would catch them days later. (Pact is the open-source tooling and Pact Broker; PactFlow is the commercial hosted broker from SmartBear. Bi-directional contract testing is a related but weaker mode that compares a provider’s own spec against consumer expectations rather than verifying the running provider, so treat it as a lighter guarantee than full consumer-driven verification.)

The gate that operationalizes this is can-i-deploy. Instead of deploying a coordinated set together, you ask the broker whether a specific version of one application is compatible with what is already deployed in a target environment:

pact-broker can-i-deploy \
  --pacticipant mobile-bff --version 4.2.0 \
  --to-environment production

It inspects the matrix of consumer and provider versions and exits 0 to allow the deploy or non-zero to block it. The Pact docs contrast this directly with the old-fashioned way that involved deploying sets of pre-tested applications together and created a bottleneck.

compatible

unverified pair

can-i-deploy: version + environment

Matrix verified?

exit 0: deploy proceeds

non-zero: deploy blocked

The same principle has an event-driven analogue. When the BFF or its services communicate over a message bus, a schema registry enforces compatibility modes that dictate deploy order. In Confluent’s Schema Registry the default is BACKWARD, which means consumers are upgraded first; FORWARD upgrades producers first; FULL satisfies both directions; and TRANSITIVE extends the check across all prior versions, beyond the immediately preceding one. That BACKWARD default is Confluent-specific; other registries such as AWS Glue and Apicurio choose their own defaults, so confirm the behavior of the registry you run rather than assuming. The contract, in synchronous or asynchronous form, is what lets a team answer the independent-deployability question that Newman frames in Building Microservices: can I change this service and deploy it by itself, without changing anything else.

Backward-Compatible Evolution Because Old Clients Linger#

Contracts tell you whether a change is safe to deploy. The asymmetric clock tells you what kind of change is safe to make: an additive, backward-compatible one. Because old clients never fully disappear, every server change has to keep working for a version you shipped months ago. The default posture for a mobile backend is therefore additive-only evolution, and a few well-understood patterns make that practical.

Semantic versioning encodes the promise in the version number itself. A MAJOR bump signals an incompatible change, MINOR signals backward-compatible additions, and PATCH signals backward-compatible fixes; the scheme only means anything against a declared public API. For a mobile backend, the implication is blunt: MAJOR bumps are the changes you are trying to avoid, because they are the ones that strand old clients. Stripe is a useful example of additive-only evolution in practice. Its date-based API versions are backward-compatible by construction, and its documentation describes upgrades within that scheme as safe to apply without breaking existing code.

When you genuinely need an incompatible change, parallel change (also called expand/migrate/contract) makes it safe without a coordinated cutover. You first expand the server to support both the old and new shapes, then migrate consumers over at their own pace, and only contract away the old shape once the clients that used it have aged out. The producer deploys once at the start and is never blocked waiting for clients to migrate, which is exactly the decoupling the mobile case demands. During the migrate phase, though, you maintain two versions of the same behavior, and on mobile that phase is long because it ends only when old clients fall below your support threshold.

Contract

Remove old shape once old clients age out

Migrate (long on mobile)

New clients use new shape

Old clients keep old shape

Expand

Server supports old + new

The client side has its own discipline: the tolerant reader. Following Postel’s Law, a client should be conservative in what it sends and liberal in what it accepts, which in practice means parsing only the fields it needs and ignoring everything it does not recognize. A tolerant client lets the server add fields freely, because an old binary that ignores unknown fields keeps working against a richer response. A field added today does not break a binary installed six months ago, which keeps additive evolution workable on the client clock.

Server-Driven UI and Its Tax#

The most aggressive way to dodge the client clock is server-driven UI (SDUI): moving not just the data but the layout and the available actions to server cadence, so the binary becomes a generic renderer of instructions the server sends. Airbnb’s Ghost Platform is a documented example, described by its team as a unified, opinionated, server-driven UI system that orchestrates layout and actions across iOS, Android, and web, with backward-compatible sections so old clients keep rendering. When it works, it collapses the client clock for whole screens: you can rearrange a feed or change a flow without shipping a binary at all.

Every UI capability the server can drive becomes a contract the server must honor for as long as old clients exist: SDUI multiplies the backward-compatibility burden of the previous section instead of escaping it. It also moves complexity that engineers understand well (typed UI code in the app) into a less familiar place (a server-driven schema and a renderer that must degrade gracefully for every version in the field). Spotify’s own history belongs to that record: it built and later deprecated HubFramework, which its repository describes as a component-driven UI framework for backend-driven layouts, not a server-driven one. Read these accounts as documented vendor experience; the teams that sustain SDUI tend to have high screen-variation rates and the platform investment to match.

ApproachServer controlsClient controlsBackward-compat / complexity cost
Data-only APIData valuesLayout, actions, renderingLowest: evolve fields additively
BFF-shaped responsesData plus response shape per experienceLayout and renderingModerate: response contracts per experience
Full SDUIData, layout, and available actionsGeneric rendering onlyHighest: every UI capability is a permanent contract

The spectrum is the decision, not a ladder you are obliged to climb. Most products are well served by the middle: a BFF shaping responses per experience, with the client owning rendering. Full SDUI is the right call when the rate of UI variation is high enough that shipping a binary per change is the dominant bottleneck, and the team can carry a renderer that stays backward-compatible across every version in the field.

Closing#

The defensible default stops short of full SDUI: a BFF at the seam, additive-only evolution with parallel change reserved for the rare incompatible break, and contract-gated independent deploys. SDUI earns its place only when the rate of UI variation clearly justifies its permanent cost. A useful next step is to audit what your oldest supported client still calls.

For the broader release-cadence picture this fits into, see Compressing Time to Production, and for the parallel argument on the security axis, see Security Without the Review Queue.

References#

Related posts