How mobile apps force users to update without breaking
How apps check version compatibility, display blocking vs non-blocking upgrade prompts, and handle API versioning to support old and new clients simultaneously.
The Problem Statement
Interviewer: "You have a mobile app with 10 million active users. You just shipped a new backend API that is not backward-compatible. Some users have auto-update enabled, some do not. How do you force users to update without crashing their current experience?"
This question tests three things: your understanding of client-server version negotiation, your ability to design a graceful degradation strategy for users on old versions, and whether you can reason about the real-world constraint of app store review delays (you cannot deploy a mobile client instantly the way you deploy a backend).
I like this question because it is deceptively simple. Most candidates say "just check the version on launch." The strong answer covers the full matrix: how the version check works, what happens when the user ignores the prompt, how the backend supports multiple client versions during the transition, and how app store review windows affect rollout timing.
Every company with a mobile app faces this problem. Uber, Spotify, WhatsApp, and banking apps all have force-update mechanisms. It is one of those "everyone needs it, nobody talks about it" infrastructure problems.
Clarifying the Scenario
You: "Good question. Let me clarify a few things before I lay out my approach."
You: "When you say 'force update,' are we talking about a hard block (the user cannot use the app at all until they update) or a soft nudge (a dismissible prompt)?"
Interviewer: "Both. I want to understand when you would use each one."
You: "Got it. And is the new API breaking because of a security fix, a data model change, or a complete redesign?"
Interviewer: "Assume it is a security fix that requires all clients to stop calling the old endpoint."
You: "And should I cover just the mobile client, or also the backend versioning strategy?"
Interviewer: "Both. I want to see the full picture."
You: "OK. I will structure my answer in three parts: the version check flow that happens on every app launch, the backend's strategy for supporting multiple API versions simultaneously, and how we handle the app store review window between deploying the backend and getting the client update approved."
This structure works well because it follows the request lifecycle: client check, backend support, and deployment timeline. The interviewer can see that you are thinking about all three actors (the mobile client, the backend, and the app store) and not just one.
My Approach
I break this into five parts:
- Config endpoint for version checks: On every app launch, the client calls a lightweight config endpoint that returns the minimum supported version, the recommended version, and whether the update is mandatory (hard block) or optional (soft prompt).
- Hard vs soft update prompts: A hard update blocks the entire UI until the user updates. A soft update shows a dismissible banner. The choice depends on why the update is needed (security fix = hard, new feature = soft).
- API versioning on the backend: The backend runs multiple API versions simultaneously during the transition period. Old clients call v1, new clients call v2. Both work until the old version is deprecated.
- App store review window management: You cannot deploy a mobile client instantly. Apple's review takes 24-48 hours, sometimes longer. The backend must support old clients for at least the review window after the new version is submitted.
- Feature flags for gradual rollout: Even after the new client is approved, you do not flip the switch for everyone at once. Feature flags let you enable the new behavior for 1%, then 10%, then 100%.
The mental model is simple: the config endpoint is your remote control for the installed base. It lets you communicate with clients you have already shipped. Without it, you are blind. You cannot tell old clients to stop doing something. You cannot warn them about a deprecation. You cannot force them to update. The config endpoint is the one piece of infrastructure that must exist before you ship your first version.
The Architecture
Here is how the pieces work together. The architecture looks simple, but the devil is in the edge cases. Let me walk through the main flow first, then we will dig into the three areas where things get complicated.
When the app launches, the very first network call goes to a lightweight config endpoint (GET /config/version). This returns a JSON payload with the minimum supported version, the recommended version, and a force-update flag. The client compares its own version against these values.
If the client version is below the minimum, the app shows a full-screen blocking dialog with a single "Update Now" button that links to the app store. The user cannot dismiss this. If the client is between minimum and recommended, a soft prompt appears, but the user can dismiss it and continue using the app in a degraded mode where new features are hidden.
On the backend, the API router reads the X-API-Version header from every request and routes to the appropriate version handler. Both v1 and v2 run simultaneously during the transition. Once the minimum version is bumped past the old client, v1 can be safely deprecated and eventually removed.
The config response payload should look something like this:
{
"platform": "ios",
"min_version": "3.2.0",
"rec_version": "3.5.0",
"force_update": true,
"message": "A critical security update is required.",
"store_url": "https://apps.apple.com/app/id123456789",
"ttl_seconds": 3600,
"features": {
"new_checkout": false,
"dark_mode": true
}
}
Notice the ttl_seconds field. This tells the client how long to trust the cached response before fetching again. And the features map lets you toggle functionality per-version without shipping a new client. This single endpoint becomes your remote control for the entire installed base. Think of it as a one-way communication channel from your engineering team to every device that has your app installed. You cannot push data to the device (that requires push notifications, which are unreliable), but you can make the device pull instructions on every launch.
The config payload is intentionally small and simple. JSON parsing is cheap on any device, and the response fits in a single TCP packet. Do not overload this endpoint with application state, analytics payloads, or personalization data. Keep it focused on version and feature config. Anything else should go through your main API.
Never make the config endpoint itself versioned or authenticated. If the user's token is expired or the config endpoint changes URL, the version check fails silently and you lose all ability to force updates. The config endpoint should be public, unauthenticated, and never change its URL.
The Version Check Flow on App Launch
This is the most important piece. Get it wrong and you either crash old clients or lose the ability to force updates when you need to.
I have seen both failure modes in production. One company deployed a backend change without the version check in place, and 30% of their users started getting 500 errors. Another company's config endpoint went down for 6 hours, and during that time they had no way to communicate with any client. Both situations are avoidable with the right design.
The flow has a critical edge case: what happens when the config endpoint is unreachable? My recommendation is to cache the last successful config response locally. If the network is down, use the cached values. If there is no cache (first launch ever, offline), let the app open normally. Never block a user solely because your config service is having a bad day.
Another edge case: version comparison logic. Semantic versioning (major.minor.patch) is the standard, but you need to compare versions numerically, not as strings. The string "3.9.0" is alphabetically greater than "3.10.0", but numerically 3.10.0 is the newer version. I have seen real bugs in production from string-based version comparison. Use a proper semver parsing library.
The config endpoint response should be small (under 1KB) and fast (under 100ms). It should not require authentication. If you put the version check behind your auth layer, users with expired tokens can never be forced to update.
Supporting Multiple API Versions Simultaneously
The backend cannot just switch from v1 to v2 overnight. During any transition, you have a mix of client versions in the wild. Here is how to manage that. In my experience, this is the most underestimated part of mobile backend engineering. Teams plan the happy path (new client talks to new API) and forget that for weeks or months, the majority of traffic still comes from old clients.
This is the part of the answer that separates mid-level engineers from senior engineers. Junior candidates think in terms of "deploy new version, everyone uses it." Senior candidates know that in mobile, you have a long tail of old versions that you cannot control. The backend must be a good host to all of them.
The key design decision: do you maintain two separate handler codebases (v1 and v2) or use an adapter layer?
I recommend the adapter pattern. Your v2 handler is the canonical implementation. Your v1 handler is a thin translation layer that converts v1 request shapes into v2 internal calls and converts v2 responses back into v1 shapes. This means you only maintain one real implementation and the v1 adapter is pure mapping logic.
The database must support both API versions. In practice, this means additive-only schema changes. Add new columns, do not remove or rename existing ones during the transition period. Once v1 is fully deprecated and no clients are calling it, you can clean up the schema.
Here is a concrete example. Say v1 returns user profiles with a single name field, and v2 splits it into first_name and last_name. The database adds first_name and last_name columns, keeping the old name column. The v2 handler reads the new columns. The v1 adapter reads first_name and last_name, concatenates them, and returns name. When v1 is sunset, you migrate any remaining data, drop the name column, and remove the adapter.
The adapter pattern also helps with testing. You can write tests that send v1 requests and verify the v1 adapter produces correct v2 translations. This gives you confidence that old clients will not break when you change the v2 implementation.
One thing I have seen go wrong: teams forget to version their error responses. The v1 client expects error payloads in a specific format ({"error": "message"}), but the v2 handler returns a different format ({"errors": [{"code": "FOO", "detail": "message"}]}). The v1 adapter needs to translate error responses too, not just success responses. Miss this and old clients show cryptic error messages when something goes wrong.
Another common oversight: pagination format changes. If v1 uses offset-based pagination (page=2&per_page=20) and v2 uses cursor-based pagination (cursor=abc123), the adapter must maintain a mapping between cursors and offsets. This gets surprisingly complex and is easy to miss during API reviews.
For your interview: say "I would use header-based versioning with an adapter layer, keep one canonical implementation, and set a sunset date for the old version." That is a complete answer.
Graceful Degradation for Old Clients
There is a middle ground between "everything works" and "hard block." During the transition period, old clients should still function, but with reduced capabilities. This is graceful degradation, and it buys you time during the adoption window.
The idea is that the backend adjusts its response based on the client version. A v3.0 client gets a minimal response with only the fields it understands. A v3.3 client gets a slightly richer response. The v3.5 client gets everything.
This is different from API versioning. API versioning is about the request/response contract. Graceful degradation is about feature availability within the same API version. A v1 client can still function, but some features are turned off because the client does not know how to render them.
The key principle: old clients should never receive data they were not designed to handle. Omit new fields rather than including them. Return old field names through the adapter layer. Degrade features by omission, not by sending error states.
Handling the App Store Review Window
This is the tricky part that separates strong answers from average ones. When you submit a mobile app update, there is a delay before users can actually install it. And that delay is completely outside your control. You cannot speed it up, you cannot predict exactly how long it will take, and you cannot skip the review process. This is the fundamental constraint that makes mobile different from web.
For web apps, deployment is instantaneous. You push a new build and every subsequent page load gets the new code. For mobile, deployment is a multi-week process involving store reviews, user update behavior, and OS-level update scheduling. Your entire architecture must be designed around this asymmetry.
The timeline matters because you cannot control it. Apple might reject your update and require resubmission. Google might take longer than usual during holidays. You must plan for the worst case.
My recommended timeline for a non-critical update:
- Day 0: Deploy backend with both v1 and v2 running. Submit client to both stores.
- Day 1-3: Both stores approve (hopefully). Bump recommended version per platform as each is approved.
- Day 7-14: Monitor adoption. Most users with auto-update enabled will have the new version by now.
- Day 14-21: Bump minimum version to force remaining users. This triggers the hard block for anyone still on the old version.
- Day 30-60: Sunset v1 API. Remove v1 handlers from the codebase.
For a critical security fix, compress this timeline. Deploy the backend fix immediately (v1 hotfix if possible, or v2 with the adapter). Submit the client with expedited review (both stores offer this for security issues). Bump the minimum version within days, not weeks.
One thing I always mention in interviews: the rollout is per-platform, not global. Android often approves faster than iOS. You might have Android users on v3.5.0 while iOS users are still waiting for Apple's review. The config endpoint must return platform-specific minimum versions. If you use a single global minimum, you end up blocking one platform while waiting for the other.
Google Play also offers an in-app update API (Play Core library) that lets you trigger updates from within the app without redirecting to the Play Store. This is smoother for the user because they do not leave the app. iOS has no equivalent, so the iOS flow always opens the App Store.
The key insight: your backend should be able to function with old AND new clients at all times. Never deploy a backend change that requires the new client, because you do not control when users get the new client.
These are the non-obvious challenges that make force-update harder than it looks. Each one is a potential follow-up question from the interviewer.
-
Platform-specific minimum versions. iOS and Android apps have different version numbers and different approval timelines. Your config endpoint should return separate minimum versions per platform, not a single global minimum. Otherwise, you end up blocking Android users (whose update was approved in 2 hours) because Apple's review is still pending. I always recommend modeling the config response as a map keyed by platform identifier (ios, android, web) so you can control each independently
-
Platform-specific minimum versions. iOS and Android apps have different version numbers and different approval timelines. Your config endpoint should return separate minimum versions per platform, not a single global minimum. Otherwise, you end up blocking Android users (whose update was approved in 2 hours) because Apple's review is still pending.
-
Users who never open the app. If a user has not opened the app in 3 months, they never hit the config endpoint. When they eventually open it, they might be 5 versions behind. Your hard block needs to handle jumps of multiple major versions, not just one increment.
-
Enterprise MDM (Mobile Device Management) delays. Corporate users often have IT departments that control app updates through MDM software. Even if the app store has the new version, the IT department might take weeks to approve it for corporate devices. If your app has enterprise customers, you need to keep old API versions alive longer than you might want. I have seen teams go further and bake a static fallback config into the app binary itself, updated with each release, so even a total backend failure cannot prevent the app from functioning.
-
Config endpoint SPOF. If the config service goes down, you lose all ability to force updates or communicate with clients. This endpoint needs to be highly available (multi-region, behind a CDN with aggressive caching, with a static fallback). Some teams serve the config from a CDN with a 5-minute TTL as the primary path, with the dynamic service as the origin. This balances security needs against user experience disruption.
-
Version check on app restore from background. On iOS, an app can stay in the background for days. When it comes to the foreground, should you re-check the version? If you do, the user might see a blocking update screen in the middle of a task. If you do not, they might use the app for hours after you needed them to update. I recommend checking on foreground but only showing the prompt if the app has been backgrounded for more than 1 hour.
-
Rollback scenarios. What if you ship a broken update? The new v3.5.0 has a critical bug that crashes the app. Users who already updated are stuck. You cannot un-publish from the App Store (you can only submit a new version). Your options: deploy a hotfix v3.5.1 through expedited review, or temporarily lower the minimum version back to 3.1.0 so users who have not updated yet are not forced to install the broken version. This is why the config endpoint should support lowering the minimum version, not just raising it.
-
A/B testing across versions. If you are running an A/B test on a feature that exists in both v1 and v2 of the API, you need the experiment assignment to be version-aware. A user on v1 should not be assigned to a treatment that requires v2's data model. Your experimentation platform needs to filter by client version.
Here are the five most common mistakes I see in interviews. Each one reveals a gap in the candidate's understanding of mobile deployment constraints versus web deployment.
What Most People Get Wrong
| Mistake | What they say | Why it is wrong | What to say instead |
|---|---|---|---|
| Backend-first thinking | "Deploy the new API and remove the old one" | Users on old clients instantly get errors. You cannot force-update a mobile client in minutes. | "Deploy v2 alongside v1. Run both until the new client has sufficient adoption, then sunset v1." |
| The root cause of most of these mistakes is web-centric thinking. On the web, you deploy a new frontend and every user gets it instantly. There is no version skew, no review window, no users stuck on old code. Mobile is fundamentally different. You ship code that you cannot take back, to devices you do not control, on a timeline you cannot accelerate. Every design decision must account for this asymmetry. | Ignoring the review window | "Push the update to the app store and bump the version" | Apple takes 24-48h to approve. You cannot bump minimum version until the update is actually available. |
| Single version number | "Set minimum version to 3.5.0" | iOS and Android are different apps with different release schedules. | "Maintain separate minimum versions per platform in the config endpoint." |
| Hard block for everything | "Just force users to update" | Hard blocks frustrate users and cause uninstalls. Use them only for security or breaking changes. | "Soft prompt for feature updates, hard block only for security fixes or breaking API changes." |
| No fallback for config failure | "Check the version on launch" | If the config endpoint is down, the app either crashes or fails open with no version enforcement. | "Cache the last config locally. If fetch fails, use cached config. If no cache exists, allow the app to open." |
How I Would Communicate This in an Interview
Here is how I would actually say this:
"The core of mobile force-update is a config endpoint that the app calls on every launch. This endpoint returns three values: the minimum supported version (below which we show a hard block), the recommended version (below which we show a soft dismiss-able prompt), and a force flag for emergencies.
The client compares its own version against these values. If it is below minimum, we show a full-screen blocker that links to the app store. The user cannot continue until they update. If it is between minimum and recommended, we show a banner they can dismiss, and we hide any features that require the new API.
On the backend, we run both API versions simultaneously. Old clients send v1 requests, new clients send v2. We use a thin adapter layer so v1 requests get translated to v2 internally, meaning we only maintain one real implementation. This runs for 30-60 days until adoption exceeds our threshold.
The hardest part is the app store review window. We cannot bump the minimum version until the new client is actually available in the store. So the timeline is: deploy backend with both versions, submit client to stores, wait for approval, bump recommended version, wait for adoption, then bump minimum version. For iOS, this process takes 2-3 weeks minimum."
I would then pause and say: "I can go deeper on any of these areas. The API versioning strategy has some interesting tradeoffs, and the failure modes around the config endpoint are worth discussing if you are interest
The key thing I want you to notice about this delivery: I started with the config endpoint (the control mechanism), then covered the backend strategy (how both versions coexist), then the deployment timeline (the constraint that makes everything hard). This matches how the problem unfolds in real life. You build the config endpoint first, then design the API versioning, then plan the rollout around the app store constraint. Leading the interviewer through the problem in chronological build order makes your answer feel natural and well-reasoned.
Another thing: whenever you reference specific numbers ("24-48 hours for iOS review," "80% adoption threshold," "30-60 day sunset"), it signals that you have actually dealt with this in production. Real practitioners know the numbers. Textbook answers do not have numbers.ed."
This framing shows the interviewer you have depth beyond the overview. Most candidates stop at the overview. By offering to go deeper, you demonstrate that the overview was a summary, not the extent of your knowledge.
Interview Cheat Sheet
- Trigger: "How do you force-update a mobile app?" β Say: "Config endpoint on every launch returns minimum and recommended versions. Below minimum = hard block. Between minimum and recommended = soft prompt. The endpoint must be unauthenticated and highly available."
- Trigger: "Hard update vs soft update?" β Say: "Hard block for security fixes and breaking API changes. Soft prompt for new features. Hard blocks cause uninstalls, so use them sparingly."
- Trigger: "How do you handle the app store delay?" β Say: "Deploy backend with both API versions first. Submit client to stores. Bump minimum version only after the update is approved and available. Plan for 24-48h iOS review, 2-6h Android."
- Trigger: "How do you version your API?" β Say: "Header-based versioning with an adapter pattern. One canonical v2 implementation, thin v1 adapter for translation. Same URLs, version distinguished by X-API-Version header."
- Trigger: "What if the config service is down?" β Say: "Cache the last config response locally. If fetch fails, use cached values. If no cache exists, allow the app to open. Never block users because your config is unreachable."
- Trigger: "How long do you support old API versions?" β Say: "30-60 days after the new client is available. Monitor adoption rate. Bump minimum version when 80%+ users are on the new version. Add Sunset headers to v1 responses."
- Trigger: "What about users who never update?" β Say: "Eventually they hit the hard block when we bump minimum version. The hard block links to the app store. If they refuse, they cannot use the app. This is the designed behavior for security-critical updates."
- Trigger: "How do you roll out the new version gradually?" β Say: "Feature flags. Even with the new client installed, new behavior can be behind a flag. Roll out to 1%, monitor error rates and crashes, then 10%, then 100%."
- Trigger: "iOS vs Android differences?" β Say: "Separate minimum versions per platform. Android reviews are 2-6 hours, iOS 24-48. You can bump Android's minimum version days before iOS. Google also supports in-app updates via Play Core library."
- Trigger: "What about enterprise users?" β Say: "MDM-managed devices have IT-controlled update schedules. Keep old API versions alive longer if you have enterprise customers. Communicate deprecation timelines clearly."
Test Your Understanding
Quick Recap
- The config endpoint is the control plane for force-update: it returns minimum version (hard block), recommended version (soft prompt), and per-platform settings on every app launch.
- Hard blocks are for security fixes and breaking API changes only. Soft prompts are for everything else. Hard blocks cause uninstalls.
- The backend must support multiple API versions simultaneously during any transition. Use an adapter pattern so v1 is a thin translation layer over the v2 canonical implementation.
- Never bump the minimum version until the new client is actually approved and available in the app store. Violating this blocks all users with no escape.
- The app store review window (24-48h for iOS, 2-6h for Android) is the hardest constraint. Plan your rollout timeline around the slowest store.
- The config endpoint must be unauthenticated, highly available, and cacheable. If it goes down, clients fail open using cached values.
- Feature flags work alongside version checks for gradual rollout. Even users with the new client version can be behind a flag until you are confident the new behavior is stable.
- Enterprise users with MDM-managed devices have slower update cycles. Factor this into your API sunset timeline. Some enterprise clients might need 90-day windows instead of the standard 30-60 days. If your app serves both consumer and enterprise segments, your sunset policy should account for the slowest segment.
Related Concepts
- How feature rollout percentage works: Feature flags control which users see new behavior, independent of client version. This pairs with force-update to give fine-grained rollout control. You might have the new app version installed but the new feature flag still off.
- How API rate limiting headers work: When old clients call deprecated endpoints at high volume, rate limiting protects the backend while the transition completes. You might rate-limit v1 more aggressively than v2 to encourage migration.
- How connection draining works: Similar concept applied to API versions. When deprecating v1, drain existing connections gracefully rather than cutting them off. In practice, this means returning
SunsetandDeprecationheaders before actually turning off the endpoint. - How CDN cache invalidation works: The config endpoint is often served from a CDN. Understanding cache TTLs and invalidation helps you reason about how fast minimum version changes propagate to clients. A 5-minute CDN TTL means it takes up to 5 minutes for a minimum version bump to reach all clients.
- How mobile apps handle offline-first sync: Closely related problem. Apps that work offline need to reconcile local data when they come back online, and the server's API version might have changed while the app was offline. The force-update check should happen before the sync attempt, not after.