Mobile system design rounds are lost in a specific way: the candidate designs the backend.

Asked to design a messaging app, they draw load balancers, a message queue, a sharded database and a fan-out service. All of it is plausible, and almost none of it is what an iOS panel wanted to hear. The interviewer is asking how the app behaves — on a phone that loses signal in a lift, gets suspended the moment the user switches apps, and has to show the right messages in the right order regardless.

Below is one prompt worked across a full 45-minute round, including the parts where the interviewer pushes.

45m
The round, and where each part of it should go
1
Source of truth for the UI: the local database
0
Sockets your app keeps open once it's suspended

The 45 minutes

MinutesWhat happensWhat it's testing
0–5Scoping questionsWhether you design for a product or for a diagram
5–15Data and the local storeWhether you know where the UI should read from
15–25Sending and orderingWhether you've thought about the network failing halfway
25–35Real-time and the backgroundWhether you know what iOS will and won't let you do
35–45Their follow-upsWhether the design bends or breaks when the scope changes

The single most common mistake is spending twenty minutes on the first two rows. Scoping is necessary and it should be fast; the interesting decisions are in the middle.

Scoping, in five minutes

"Design a messaging app for iOS."

Five questions, and say why each one matters as you ask it — that's what turns a list of questions into evidence of judgement:

"One-to-one only, or groups — and how big? A group of 500 changes how I'd handle read receipts. Text only, or images and video? Media means background uploads, which is its own problem on iOS. One device per user, or several? Multi-device means read state lives on the server, not the phone. End-to-end encryption? That changes what a push notification can contain. And how much history should work offline — everything, or the last month?"

Then assume, out loud, so the interviewer can redirect you cheaply:

"I'll take one-to-one and groups up to a hundred, text and images, one device per user for now, no end-to-end encryption in v1 — I'll come back to what it would change — and the last thirty days available offline."

The local database is the source of truth

The decision that shapes everything else, and the one to state as a principle rather than discover by accident:

"The UI never reads from the network. It reads from a local database, and the network's only job is to write into that database. When a message arrives — over the socket, from a sync, from a push — it goes into the store, and the conversation screen updates because it's observing the store."

That buys three things in one move: offline reading is free, there's one code path to render a conversation instead of three, and the screen can't show two different versions of the same message.

Then say what it costs, because the panel is listening for it:

"The price is schema migrations I'll be living with for years, and storage. Two hundred conversations with five hundred messages each, at about a kilobyte per message once you count metadata, is roughly a hundred megabytes. That's fine for recent history and wrong for everything — so I keep a window locally and page older messages from the server when someone scrolls up."

"The UI never reads from the network" is the sentence the round is built around. Candidates who reach for "the view model calls the API and shows the result" are designing a screen, not an app, and every follow-up from here — offline, ordering, push — becomes a special case bolted onto the side. Stated as a rule at minute five, it turns those same follow-ups into one sentence each: the event writes to the store, and the UI follows.

Sending, when the network fails halfway

A message has a lifecycle, and the design has to survive every transition being interrupted.

StateMeaningShown as
PendingIn the local store, not yet acknowledgedA clock icon, in the conversation already
SentServer acknowledged itOne tick
DeliveredReached the recipient's deviceTwo ticks
ReadRecipient opened the conversationTwo ticks, coloured
FailedRetries exhaustedRed mark, tap to retry

The detail that separates strong answers:

"When the user taps send, I write the message to the store immediately with a client-generated ID, so it appears instantly. Then I send it with that same ID. If the request times out I retry — and because the ID is mine, the server can recognise a retry and not create a duplicate. Without that, a slow network turns one message into three."

That's idempotency, described in terms of a user-visible bug rather than a definition. It's also the thing interviewers most often probe next: "what if the server got it but the acknowledgement was lost?" — which the client-generated ID already answers.

Ordering, without trusting the clock

"I'd never order by the device's timestamp. Phone clocks are wrong, and two people's phones are wrong by different amounts. The server assigns each message a sequence number per conversation, and that's the order.

My own pending messages don't have a sequence number yet, so they sit at the bottom in the order I sent them, and move into place when the server acknowledges them.

And the sequence number gives me something better than ordering: gap detection. If the last message I have is 102 and the socket delivers 105, I know I've missed two and I ask the server for 103 and 104. Without that, a dropped socket frame is a message that silently never appears."

Gap detection is the senior signal in this section. Most candidates get ordering right; far fewer notice that the same number tells you when you've lost something.

What iOS will and won't let you do

This is the section that distinguishes an iOS engineer from a backend engineer drawing an iOS app.

"While the app is in the foreground, I keep a WebSocket open for real-time delivery. When the user leaves, iOS suspends the app within seconds — a bit longer if I ask for extra time to finish something — and the socket is gone. I wouldn't fight that. Trying to keep a connection alive in the background is the classic way to get an app rejected or drain a battery.

So in the background, push notifications do the job: the server sends a notification for new messages, and the user sees it without my app running. When the app comes back to the foreground, it reconnects and asks for everything after the last sequence number it has — the same gap-filling path as before, just with a bigger gap."

Two specifics worth naming because they show you've built this:

  • A notification service extension can modify a notification before it's shown — fetching a sender's avatar, or, if the messages are end-to-end encrypted, decrypting the preview on the device, because the server can't put readable text in the push.
  • Image sends go through a background transfer session, so a photo the user sent just before locking their phone keeps uploading after the app is suspended, and the system wakes the app when it finishes.

The follow-ups

"Now the user has an iPad as well." — "Read state can't live on the phone any more. Each device keeps its own read position, the server holds the authoritative one, and the badge count comes from the server in the push payload rather than being computed locally. The local-store design survives this unchanged, which is a good sign."

"Add end-to-end encryption." — "Three things change. Push can't contain the message, so the extension decrypts the preview on the device. The server can't search, so search becomes local and limited to the history I keep. And multi-device gets much harder, because each device needs its own keys — I'd want to scope that separately rather than pretend it's a flag."

"You have two weeks for a v1. What do you cut?" — the question checking whether you can prioritise your own design: "Typing indicators, read receipts and local search. I'd keep the client-generated IDs and the sequence numbers, because they're cheap now and very expensive to retrofit once there's data in the wild."

The backend answer

Designing the servers

“A load balancer in front of a chat service, messages through Kafka, stored in Cassandra sharded by conversation ID, with a fan-out service for groups.”

Reasonable for a backend round and almost entirely off-topic for this one. The panel learns nothing about how the app behaves when the network drops or iOS suspends it — which is the only thing they were asking.

The iOS answer

Designing the device

“The UI reads only from the local store. Messages get client IDs for safe retries and server sequence numbers for ordering and gap detection. In the background, push does the work.”

Three decisions, each tied to a failure a real user would see. The server stays a box on the whiteboard, which is exactly the right amount of attention for it in this round.

Saying it out loud

Draw as you talk, and draw the data flow rather than the classes: server, socket and push on one side, the local store in the middle, the UI on the other side. Every decision above is an arrow on that picture, and the interviewer can follow your reasoning by watching where the arrows go.

Label the arrows with verbs — writes, observes, fills gaps — rather than technology names. It keeps the conversation on decisions instead of frameworks, and if English isn't your first language it's also much easier to keep fluent: a verb on an arrow is a sentence you've already half-said.

The single-screen version of this question — one screen that loads, caches and works offline — is question 17 in iOS Developer Interview Questions, and it's worth having both: panels often start there and widen the scope to a whole app. The Android round asks a different design question with the same underlying instinct: the Android system design interview.

Practical target: draw the diagram from memory — server, socket, push, local store, UI — and check one thing. If any arrow goes from the network straight to the UI, the design has lost its source of truth, and that's the arrow the interviewer's first follow-up will be aimed at. Fix it on paper, then explain the diagram out loud in under three minutes.