Real-Time Messaging System Design
Design a real-time partner-traveler messaging system for a 60-min interview.
Introduction
30 Second Summary
Every time you send a message and see the "delivered" check appear a split second later, a complex system is managing connections, retries, and state behind the scenes. Designing that system in a 60-minute interview is one of the hardest mobile engineering challenges.
In this project, you will design a real-time partner-traveler messaging system for the Expedia Partner Central app. You will build structured diagrams, articulate tradeoffs at every architectural decision point, and practice delivering the full design in a format ready for Cindy's interview.
What You'll Build
You will walk into Cindy's interview able to deliver a complete messaging system design in under 15 minutes, drawing clear diagrams, naming tradeoffs at every fork, and fielding follow-up questions on performance, security, and AI/ML with prepared one-sentence answers.
By the end of this project, you'll have:
- A layered architecture diagram you can draw on a whiteboard in under 3 minutes, covering the full messaging stack from SwiftUI views through to WebSocket infrastructure.
- A 6-state WebSocket connection state machine with labeled transitions that deterministically handles iOS background transitions, network drops, and token refresh.
- Articulated tradeoffs at every architectural decision point with one-sentence answers ready for Cindy's follow-up questions on transport choice, delivery guarantees, API style, security, and AI/ML integration.
- Secret Mission: Design the notification delivery and background sync layer using APNs silent push and BGAppRefreshTask to handle messages received while the app is suspended.
Are there any prerequisites?
No coding experience is required. You will not write Swift or open Xcode. This project teaches you the vocabulary, diagrams, and reasoning patterns that system design interviewers expect.
Familiarity with general mobile app concepts (what a view is, what an API call does, what a database stores) helps but is not mandatory. Every iOS-specific term is explained in plain language when it first appears.
Before We Start
Before we dive in, let's lock in what you're about to design and how it builds on Project 5's offline-first foundation.
This project fills the gaps Cindy will probe: real-time communication, API contracts, performance optimization, security architecture, and AI/ML integration.
Clarify Requirements and Draw the Architecture
The first thing Cindy evaluates is whether you can scope a problem under time pressure. Without clear requirements and boundaries, you will design a system that solves the wrong problem.
In this step, you will define what the messaging system must do, draw the complete architecture in Excalidraw, and articulate why the architecture uses a repository coordination pattern. This gives you a clear diagram to redraw on a whiteboard during the interview.
How to read model and class names in this project
Throughout this project you will see names like MessageRepository, WebSocketManager, and ConversationListView. These are labels for responsibilities, not code you write. Think of each name as a job title in a company: the Repository is the "coordinator", the Manager is the "connection handler", the View is the "display screen".
In the interview, you draw boxes on a whiteboard, write these names inside, and draw arrows showing how data flows between them. The interviewer evaluates whether you understand what each piece does and why it exists, not whether you can implement it in Swift.
When you see iOS-specific API names (like SecItemAdd or NSCache), treat them as vocabulary words to drop in your answer. Each one is explained in plain language in this project so you understand what it does without needing to code it.
Define functional and non-functional requirements
Imagine Cindy says: "Design the real-time messaging feature for Partner Central. Partners need to communicate with travelers about their reservations." Your first move is to ask clarifying questions and write down requirements.
- Switch to your notes document.
- Write a heading called "Functional Requirements" and add these 7 items:
- Partners send and receive text messages with travelers in real-time (per-reservation conversation threads).
- Messages display delivery status (sent, delivered, read).
- Partners see typing indicators when a traveler is composing.
- Message history loads with pagination (older messages on scroll-up).
- Partners receive push notifications for new messages when the app is backgrounded.
- Images and attachments can be sent and received.
- Smart reply suggestions appear based on message context (AI-powered).
- Below that, write a heading called "Non-Functional Requirements" and add these 4 items:
- Latency: messages appear within 500ms of sending on a good connection.
- Reliability: no message loss even during network transitions (Wi-Fi to cellular).
- Security: all communication encrypted in transit (TLS), tokens stored securely, certificate pinning for API endpoints.
- Performance: message list scrolls at 60fps, images load progressively without blocking the UI.
- Finally, write a heading called "Scope Boundaries" and add these two sections:
- In scope: 1:1 messaging per reservation, text + images, real-time delivery, typing indicators, push notifications, message history pagination, smart reply suggestions.
- Out of scope: group conversations, video/audio calls, message editing/deletion, end-to-end encryption, read receipts across multiple partner devices.
Why define scope boundaries in the interview?
Scope boundaries communicate that you can prioritize under time pressure. Stating what is out of scope shows Cindy you will not over-engineer a solution that cannot be delivered.
In a 60-minute interview, you have roughly 3 minutes for requirements. Practice saying these aloud to feel the pacing.
Draw the high-level architecture diagram
The architecture diagram is the centerpiece of your delivery. Practice drawing this in under 3 minutes because that is approximately how long you will have in the interview before Cindy expects you to move to the deep-dive.
- Switch to your Excalidraw canvas in the browser.
- Draw 6 horizontal layers stacked top to bottom. Label each layer with its name on the left side.
Here are the 6 layers and their components, from top to bottom:
- UI Layer: ConversationListView, MessageThreadView, ComposeView (all SwiftUI views).
- ViewModel Layer: ConversationListViewModel, MessageThreadViewModel (both @Observable, @MainActor).
- Service Layer: MessageService (orchestrates send/receive), PresenceService (typing indicators), NotificationService (push handling), SuggestionService (AI-powered reply suggestions).
- Repository Layer: MessageRepository (coordinates REST, WebSocket, and local persistence). Single source of truth pattern: DB is truth, ViewModels read from DB only.
- Data Sources: RemoteDataSource (REST API with cursor pagination), WebSocketDataSource (real-time event stream), LocalDataSource (SwiftData with upsert behavior).
- Infrastructure: WebSocketManager (connection lifecycle), APIClient (URLSession + Codable), KeychainManager (token storage), ImageCacheManager (3-tier cache), NetworkMonitor (NWPathMonitor), MLModelManager (Core ML inference).
- Now draw directional arrows between the layers to show data flow:
- UI reads from ViewModel (arrow pointing down from UI to ViewModel).
- ViewModel reads from Repository (arrow pointing down from ViewModel to Repository).
- Repository coordinates three Data Sources (arrows pointing down from Repository to each of the three data sources).
- Infrastructure provides transport to the Data Sources (arrows pointing up from Infrastructure to Data Sources).
Your diagram should now show all 6 layers with named components and directional arrows connecting them.
What does each layer do?
The UI Layer renders conversation state. The ViewModel Layer transforms repository data into view-ready state. The Service Layer contains domain-specific logic for sending, presence, notifications, and suggestions.
The Repository Layer is the coordination hub. It receives real-time events from the WebSocket, REST responses from pagination calls, and reads from the local store. The Data Sources layer implements the actual network and persistence operations. Infrastructure provides low-level transport and managers that multiple layers depend on.
💡 Practice speed
Set a 3-minute timer and try redrawing this diagram from memory. In the interview, you will not have this reference in front of you. The goal is to reproduce the 6 layers and arrows from recall.
Articulate the repository coordination pattern
Knowing the components is not enough. Cindy will ask why you chose this layered design over a simpler approach. Having a prepared rationale saves you from stumbling through an answer.
- Switch back to your notes document.
- Write a heading called "Architecture Rationale" and add this explanation:
Write the following paragraph in your own words:
"The Repository is the coordination hub. It receives real-time events from the WebSocket, REST responses from pagination calls, and reads from the local store. All writes go through upsert to the local DB first. ViewModels only read from the DB, never from in-flight API responses. This means the UI never shows stale or inconsistent state even during rapid message delivery."
- Below that paragraph, write the tradeoff against a simpler approach:
"You could skip the repository and have ViewModels talk directly to the WebSocket. The tradeoff is testability and consistency: without the repository, you would need to handle deduplication, ordering, and persistence in every ViewModel individually."
Why does the single source of truth matter for messaging?
In a messaging app, the same message can arrive from multiple paths: the WebSocket delivers it in real-time, and a REST call fetches it during history pagination. If ViewModels read directly from both sources, the same message could appear twice in the UI or in a different order.
By routing everything through the local DB with upsert-by-UUID, duplicates are eliminated at the persistence layer. ViewModels only read from one place, so the UI is always consistent.
Check your notes. You should have: 7 functional requirements, 4 non-functional requirements, scope boundaries (in/out), and a rationale paragraph with the tradeoff clearly stated.
✔️ I've got all sections in my notes
Your notes and diagram are the foundation for every step that follows. Each subsequent step deep-dives into one layer of this architecture.
ⓧ I'm missing some items
Scroll back through this substep and compare your notes against the lists above. Make sure you have all 7 functional requirements, all 4 non-functional requirements, both scope sections, and the rationale paragraph with its tradeoff.
You now have the skeleton of your entire system design delivery. Next up, you will dive into the hardest component to explain: the WebSocket connection state machine that handles disconnection, reconnection, and iOS background transitions.
Design the WebSocket Connection State Machine
You have your requirements locked in and the full architecture drawn. Now you need to answer the hardest question in real-time communication: what happens when things go wrong?
The naive approach is to open a WebSocket and read messages. That breaks the moment the app backgrounds, the network switches from Wi-Fi to cellular, or a token expires. Cindy will probe exactly these edge cases. A state machine gives you a deterministic answer for every scenario she can throw at you.
Draw the WebSocket connection state machine in Excalidraw
A connection state machine makes every edge case predictable. Instead of scattered if-else checks, you have named states with explicit transitions. This is what you will draw on Cindy's whiteboard.
What is a state machine in plain terms?
Think of a traffic light. It has three states: red, yellow, green. It can only be in one state at a time, and there are strict rules about which state comes next (green never jumps to red without going through yellow).
A connection state machine works the same way. Your app's internet connection is always in exactly one named state (like CONNECTED or DISCONNECTED). Each state has rules for what triggers a change to the next state. Drawing this on a whiteboard shows the interviewer you have a clear, predictable answer for every "what if" question they can throw at you.
- Switch back to your Excalidraw canvas from the previous step.
- Find an open area below or beside your architecture diagram.
- Draw 6 boxes (rectangles) and label each one with a state name:
- DISCONNECTED (the starting state when the app launches or is backgrounded).
- CONNECTING (handshake in progress).
- CONNECTED (socket open, messages flowing).
- BACKING_OFF (waiting before retry after a failure).
- RECONNECTING (attempting a fresh handshake after backoff).
- DRAINING_QUEUE (reconnected but sending queued messages before resuming normal flow).
Now draw labeled arrows between the boxes for each transition:
- DISCONNECTED → CONNECTING: label it "app foregrounds OR network becomes available".
- CONNECTING → CONNECTED: label it "handshake succeeds (didOpenWithProtocol)".
- CONNECTING → BACKING_OFF: label it "handshake fails or timeout".
- CONNECTED → DISCONNECTED: label it "app backgrounds (proactive close)".
- CONNECTED → BACKING_OFF: label it "connection drops (network change, server close, ping timeout)".
- BACKING_OFF → RECONNECTING: label it "backoff timer fires (500ms → 1s → 2s → 4s, capped 30s, 0-50% jitter)".
- RECONNECTING → CONNECTED: label it "new handshake succeeds".
- RECONNECTING → BACKING_OFF: label it "handshake fails again".
- CONNECTED → DRAINING_QUEUE: label it "reconnection succeeds but queued messages exist".
- DRAINING_QUEUE → CONNECTED: label it "all queued messages confirmed delivered".
Why these specific states?
Most tutorials show only 3 states (disconnected, connecting, connected). The BACKING_OFF state prevents reconnection storms where thousands of clients hammer the server simultaneously after an outage.
DRAINING_QUEUE handles the case where messages were queued locally during disconnection and need to be sent in order before normal operation resumes. In production systems, this state machine pattern improved delivery rates from roughly 94% to 99.97%.
Step back and look at your diagram. You should see 6 labeled boxes connected by 10 labeled arrows forming a clear cycle with two failure-recovery loops (BACKING_OFF ↔ RECONNECTING).
Practice drawing this quickly
In the interview, you will have about 3 minutes to draw this on a whiteboard. Practice redrawing it from memory until you can do it without referring to your notes.
🙋♀️ Diagram looks cluttered?
Arrange the states in a rough circle or horseshoe shape with CONNECTED at the top. Place BACKING_OFF and RECONNECTING side by side in the middle to show the retry loop clearly.
If arrows overlap, use curved lines or offset the connection points on each box.
Articulate the real-time transport tradeoffs
Cindy may ask why you chose WebSocket over other real-time transport options. Having a clear comparison ready shows you evaluated alternatives before committing.
- Open your notes document and add a new section titled "Transport Comparison".
- Write the following comparison for each transport option:
WebSocket (your choice):
- Bidirectional, persistent connection with low latency once established.
- Both client and server can push at any time.
- Requires manual reconnection and heartbeat logic.
Server-Sent Events (SSE):
- Unidirectional (server to client only) with automatic reconnection built in.
- Simpler and firewall-friendly, but client cannot send messages over the same channel.
- Not suitable for full-duplex chat where both sides send frequently.
Long Polling:
- HTTP request that blocks until the server has data.
- Simple to implement but high overhead from repeated TCP handshakes.
- Higher latency and wasteful of server resources at scale.
- Now add the iOS-specific implementation comparison beneath the transport options:
Socket.IO:
- Built-in reconnection, acknowledgements, and cross-platform compatibility (web + mobile).
- Adds protocol overhead, requires a Socket.IO server, and is a third-party dependency.
URLSessionWebSocketTask (native):
- Native to iOS, zero dependencies, full control over the connection lifecycle.
- Requires manual reconnection, heartbeat, and more boilerplate.
- Apple recommends Network framework (NWConnection with NWProtocolWebSocket) for new WebSocket code per TN3151, but URLSessionWebSocketTask is simpler to explain in an interview.
- Write your decision rationale beneath the comparison:
"For Partner Central, I choose native URLSessionWebSocketTask because we already use URLSession for REST, the app is iOS-only (no web parity needed), and we want zero third-party dependencies in the networking layer. The manual reconnection overhead is acceptable because we need a custom state machine anyway to handle iOS background transitions."
What about Network framework?
Apple's TN3151 says "Unless you have a specific reason to use URLSession, use Network framework for new WebSocket code." Network framework provides better connection state visibility through NWConnection states.
Either answer is valid in the interview. URLSessionWebSocketTask is more commonly discussed in practice and simpler to whiteboard. Mentioning both options and explaining your choice shows depth.
Articulate message delivery guarantees
Delivery guarantees determine whether messages can be lost, duplicated, or delivered exactly once. Cindy will expect you to name the level you chose and explain why.
- In your notes, add a section titled "Delivery Guarantees" and write down the three levels:
At-most-once: fire and forget.
- Message sent once with no retry.
- Fastest, but messages can be lost.
- Use for: typing indicators and presence updates (ephemeral data where loss is acceptable).
At-least-once: client retries until server acknowledges.
- Messages guaranteed to arrive but may duplicate.
- Deduplication via message UUID at the persistence layer handles duplicates.
- Use for: chat messages where loss is unacceptable.
Exactly-once: requires coordination with idempotency keys and server-side deduplication.
- Expensive and complex to implement.
- Use for: financial transactions, not chat.
- Write your decisions below the three levels:
- Chat messages: at-least-once delivery. Each message has a UUID assigned client-side. Server acknowledges with the same UUID. If no ACK within 5 seconds, the client retries. The local DB upserts by UUID, so duplicates from retries are harmless.
- Typing indicators: at-most-once delivery. Stale typing state is worse than a missed indicator. No retry needed.
- Add one final note about gap detection:
"Sequence IDs enable gap detection. After reconnect, client sends last-received sequence ID. Server replays all events since that sequence. This achieves gap-free delivery without exactly-once complexity."
Why not exactly-once for chat?
Exactly-once requires distributed consensus between client and server, which adds round trips and complexity. For chat, duplicates are a cosmetic problem solved cheaply by UUID upsert. Lost messages are a trust problem that damages the product. At-least-once with deduplication gives you reliability without the coordination overhead.
Design the iOS background handling strategy
iOS will suspend your app shortly after it backgrounds, which silently kills the WebSocket connection. Designing for this constraint is what separates a production-ready answer from a demo-day answer.
- In your notes, add a section titled "iOS Background Handling" and write the strategy:
When the app backgrounds:
- Proactively close the WebSocket connection. Do not wait for the OS to suspend and kill it silently.
- This avoids half-open connections where the server thinks the client is still connected.
- Use APNs silent push notifications to wake the app when new messages arrive while backgrounded.
When the app foregrounds:
- Reconnect immediately and authenticate with a fresh token.
- Resubscribe to active conversations.
- Fetch missed messages via REST using cursor-based pagination with the last-acknowledged sequence ID.
- Write the tradeoff explanation below your strategy:
"Why proactively close instead of using a UIApplication background task to keep it alive? WebSocket tasks are not supported in background URLSession configurations (confirmed by Apple DTS). UIApplication background tasks are limited to a few minutes and are not guaranteed. The reliable pattern is: design for suspension and reconnect rather than perpetual sockets."
What to say in the interview
When Cindy asks about backgrounding, say: "I design for suspension and reconnect rather than perpetual sockets. The OS will kill the connection anyway. A clean close avoids half-open state, and silent push handles the gap."
This shows you understand the iOS execution model rather than fighting it.
💡 Why not keep the socket alive in the background?
WebSocket tasks are not supported in background URLSession configurations. Apple DTS confirmed this constraint explicitly. Even UIApplication background tasks only extend execution by a few minutes before the system suspends your process.
The correct iOS pattern is to accept disconnection gracefully, rely on push for background delivery, and reconnect cleanly when the app returns to the foreground.
Before you move on, do a quick self-test. Can you answer these three questions from memory without looking at your notes?
- What happens when the app backgrounds? (Answer: proactive close, transition to DISCONNECTED.)
- What happens when the network drops mid-conversation? (Answer: transition to BACKING_OFF, exponential backoff with jitter, then RECONNECTING.)
- What happens when the token expires during reconnection? (Answer: the WebSocket manager detects the close, transitions to BACKING_OFF, refreshes the token, then reconnects with the new access token.)
Can't answer from memory?
Look at your state machine diagram and trace the path for each scenario with your finger. The visual path from state to state is easier to remember than text. Practice tracing each failure scenario three times.
Your WebSocket design now handles every failure mode deterministically. Next up, you will define the API contract that messages actually travel through and lock down the authentication flow that protects them.
Define the API Contract and Authentication Flow
Your WebSocket state machine handles the connection lifecycle. Now you need to define what actually flows through that connection, and how the client authenticates before opening it.
Cindy's evaluation topics include "API design and integration." A clean API contract shows you think about versioning, pagination, error handling, and the split between REST and real-time channels. The authentication flow ties security into the architecture you have already drawn.
Define the REST + WebSocket split and articulate the GraphQL tradeoff
A real-time messaging system needs two communication channels. REST handles state queries and history retrieval. The WebSocket handles real-time event delivery. Defining where each responsibility lives prevents confusion during the interview.
- In your notes document, add a new section titled "API Contract" and write these REST endpoint definitions:
## REST Endpoints
GET /v1/conversations
— List conversations for this partner (cursor-based pagination)
GET /v1/conversations/{id}/messages?cursor={cursor}&limit=20
— Message history (cursor-based, newest-first)
POST /v1/conversations/{id}/messages
— Send a message (HTTP fallback when WebSocket unavailable)
POST /v1/media/upload
— Upload image, returns a media URL
Why four endpoints?
The GET endpoints serve history and listing. The POST message endpoint acts as a fallback when the WebSocket is down (e.g., during reconnection). The media upload is separate because image payloads are large and need multipart encoding, not JSON.
- Below your REST endpoints, add the WebSocket event envelope schema:
{
"type": "message.new",
"payload": { },
"sequenceId": 12345,
"timestamp": "2025-01-15T10:30:00Z"
}
What does this schema represent?
Every WebSocket event shares this typed envelope. The type field tells the client which handler to invoke. The sequenceId enables gap detection after reconnection. The timestamp uses ISO 8601 for cross-platform consistency.
All event types: message.new, message.delivered, message.read, typing.start, typing.stop, presence.online, presence.offline.
- Write the following pagination rationale in your notes:
## Cursor-Based Pagination (vs Offset-Based)
In a real-time system, new messages arrive constantly.
Offset-based (page 2 = items 21-40) shifts when new items
are inserted, causing duplicates or missed items.
Cursor-based (give me messages after cursor X) is stable
regardless of insertions. The cursor is opaque — the server
can change its underlying implementation without client changes.
- Add the infinite scroll implementation strategy below that:
## Infinite Scroll Implementation
- Trigger: user scrolls within 5 messages of the top
- Fetch: next page using oldest message's cursor
- Insert: above current scroll position using ScrollViewReader
to maintain reading position (no view jumping)
- New messages from WebSocket: append at bottom, visible
immediately without disturbing history scroll position
- "New messages" badge at bottom when user is scrolled up
Why does scroll position anchoring matter?
Without anchoring, loading older messages pushes the current reading position down. The user loses their place. ScrollViewReader lets you pin the viewport to the message the user was reading while new content loads above it.
- Now write the GraphQL vs REST tradeoff table. This is what you articulate if Cindy asks "Why not GraphQL?":
## GraphQL vs REST Tradeoff
GraphQL advantages:
- Query exactly the fields you need (reduces payload)
- Fragments for reusable field sets
- Persisted queries reduce bandwidth
- Subscriptions for real-time (Apollo iOS, graphql-transport-ws)
- Normalized cache deduplicates entities across queries
GraphQL disadvantages:
- Extra complexity (schema management, code generation)
- Apollo iOS adds a significant dependency
- Learning curve for the team
- Caching more complex than URL-keyed HTTP caching
- Add your decision statement below the tradeoff table:
Decision: REST + WebSocket is simpler and sufficient for
messaging. Payloads are small and predictable. GraphQL's value
shows more on screens that aggregate data from multiple
services (partner dashboard). If the team already uses GraphQL,
I would use Apollo iOS subscriptions instead of raw WebSocket.
For a new feature, REST + WebSocket avoids the dependency.
Review your notes. You should now have REST endpoints, the event envelope schema, all seven event types, pagination rationale, infinite scroll strategy, and the GraphQL tradeoff with a clear decision.
Not sure what to say if Cindy pushes on GraphQL?
The key pivot is: "If the team already uses Apollo iOS, subscriptions are a great fit. But for a new messaging feature without existing GraphQL infrastructure, REST + WebSocket avoids adding a large dependency for simple, predictable payloads."
help me articulate when GraphQL subscriptions are better than raw WebSocket for a mobile messaging feature
Design the OAuth 2.0 token flow with Keychain storage
Every API call and every WebSocket connection needs authentication. The token lifecycle determines how the app stays authenticated across sessions, background transitions, and reconnections.
- Add a new section in your notes for the token lifecycle:
## OAuth 2.0 Token Lifecycle
- On login: receive access_token (~15 min) and
refresh_token (~30 days)
- Store both in Keychain (encrypted at rest)
- On API call: attach access_token as Bearer in
Authorization header
- On 401 response: use refresh_token to get new
access_token. If refresh fails → redirect to login
- On WebSocket reconnect: refresh access_token BEFORE
opening new connection (send fresh token in handshake)
Why refresh before reconnect?
If the token expired while the app was backgrounded, opening a WebSocket with a stale token triggers an immediate server rejection. Refreshing first avoids a wasted handshake round-trip and an extra state transition through BACKING_OFF.
- Write the Keychain storage pattern. These are the exact API details to reference in the interview:
## Keychain Storage Pattern
Class: kSecClassGenericPassword
Service: kSecAttrService = "com.expedia.partnercentral"
Account: kSecAttrAccount = "oauth-access-token" (or
"oauth-refresh-token")
Access: kSecAttrAccessibleAfterFirstUnlock
(available on background wake)
API calls:
- SecItemAdd: initial token storage after login
- SecItemUpdate: store refreshed token
- SecItemCopyMatching: retrieve token for API calls
- SecItemDelete: remove tokens on logout
In plain terms: what are these Keychain APIs?
The Keychain is iOS's built-in locked vault. It encrypts sensitive data (like login tokens) so no other app or person can read them, even if they have physical access to the device.
The four API calls are just CRUD operations on that vault: SecItemAdd = put something in, SecItemCopyMatching = look something up, SecItemUpdate = replace with a newer version, SecItemDelete = remove it. In the interview, naming these four shows you know exactly how iOS handles credentials.
The accessibility setting (kSecAttrAccessibleAfterFirstUnlock) just means "available even when the phone is locked." You need this because the app might wake up in the background to fetch new messages while the user's phone is in their pocket.
💡 What do these Keychain attributes mean?
Setting both kSecAttrService and kSecAttrAccount ensures item uniqueness in the Keychain. The kSecAttrAccessibleAfterFirstUnlock accessibility level means the token is available even when the device is locked, which is necessary for background wake scenarios (silent push fetching messages).
- Add the Keychain vs UserDefaults tradeoff. State this definitively in the interview:
## Keychain vs UserDefaults for Tokens
UserDefaults: unencrypted, readable by anyone with
device access. Never acceptable for auth tokens.
Keychain: encrypts data at rest using device hardware key.
The only acceptable choice for authentication tokens.
In the interview, state this as a fact, not a preference.
Check your notes. You should have the full token lifecycle, Keychain storage pattern with all four API calls, and the UserDefaults tradeoff written as a clear, non-negotiable position.
Confused about when each SecItem call is used?
Think of it as a CRUD mapping. SecItemAdd is Create (first login). SecItemCopyMatching is Read (every API call). SecItemUpdate is Update (token refresh). SecItemDelete is Delete (logout).
help me remember the Keychain SecItem API mapping for an interview
Design the certificate pinning approach
Certificate pinning prevents man-in-the-middle attacks by verifying the server's certificate against a known hash, rather than trusting any certificate authority. iOS offers two approaches with different tradeoff profiles.
- Write the two pinning options and your decision in your notes:
## Certificate Pinning Approach
Option 1: NSPinnedDomains (Info.plist based)
- Declare SPKI-SHA256 hashes in the plist
- System enforces pinning automatically
- Simple, no code needed
- Covers all URLSession traffic
Option 2: URLSessionDelegate-based pinning
- Implement urlSession(_:didReceive:completionHandler:)
- Check authenticationMethod == ServerTrust
- Extract server cert chain, compare SPKI hashes
- More control, harder to bypass
- Add your decision statement below the options:
Decision: Use NSPinnedDomains for the base layer
(system-enforced, zero code, covers all URLSession traffic).
Add delegate-based validation for the WebSocket connection
specifically, since it handles authentication tokens and is
a higher-value target.
Tradeoff: implementation complexity vs defense depth.
The WebSocket carries auth tokens on every handshake,
making it worth the extra code for stronger pinning.
Why layer both approaches?
NSPinnedDomains gives you broad coverage with zero code. Every URLSession request to pinned domains is automatically verified. But the WebSocket connection is a persistent, high-value channel that carries authentication tokens on every reconnect handshake. The delegate-based approach adds defense-in-depth specifically where the attack surface is largest.
Review your full notes for this step. You should have the complete API contract section covering REST endpoints, WebSocket envelope, pagination, infinite scroll, and the GraphQL tradeoff. You should also have the authentication section covering token lifecycle, Keychain storage, and certificate pinning.
✔️ Awesome, I've got everything!
Great. Confirm your notes now contain all four REST endpoints, the WebSocket event envelope with all seven event types, the pagination and infinite scroll strategy, the GraphQL tradeoff with your decision, the OAuth token lifecycle, the Keychain pattern with all four SecItem calls, and the certificate pinning decision.
ⓧ I'd like to double check the full notes
Scroll through your notes and confirm you have these sections in order:
- REST Endpoints (4 endpoints with methods and paths).
- WebSocket event envelope schema (type, payload, sequenceId, timestamp).
- All seven event types listed.
- Cursor-based pagination rationale vs offset-based.
- Infinite scroll implementation (trigger, fetch, insert, new message handling).
- GraphQL vs REST tradeoff table with decision.
- OAuth 2.0 token lifecycle (access 15 min, refresh 30 days).
- Keychain storage pattern (class, service, account, accessibility, 4 API calls).
- Keychain vs UserDefaults tradeoff.
- Certificate pinning: NSPinnedDomains + delegate-based for WebSocket, with decision.
Your API contract and security architecture are documented. Next up, you will deep-dive into the performance patterns that make a messaging UI feel fast: the 3-tier image caching pipeline, infinite scroll performance, and AI/ML integration for smart reply suggestions.
Deep-Dive: Performance and AI/ML Patterns
Your API contract and security architecture are documented. Now you need the performance and intelligence layer that makes the messaging experience feel instant and smart.
Cindy's interview topics include "Performance optimization" and the job description mentions AI/ML integration. A messaging UI must scroll at 60fps. Images must load without jank. Smart features like reply suggestions differentiate the app from competitors. This step gives you concrete patterns to articulate for all three.
Design the 3-tier image caching pipeline
Partners send images of rooms, amenities, and reservation details to travelers. Without a caching strategy, every image requires a network fetch. That means visible loading spinners, janky scrolling, and wasted bandwidth on repeated views.
A layered cache solves this by checking the fastest source first and falling back progressively. Each tier trades capacity for speed.
- In your notes document, add a new section titled "Image Caching Pipeline" and write the three tiers below.
Document each tier with its characteristics:
- Tier 1 (Memory): NSCache stores decoded UIImage objects keyed by URL. Access time is ~0ms. Evicted automatically under memory pressure. Capacity: ~50MB or ~100 images.
- Tier 2 (Disk): Stores encoded image data (JPEG/PNG bytes) in a dedicated cache directory. Access time is ~5-10ms (SSD read). TTL: 7 days. Disk budget: 200MB with LRU eviction when exceeded.
- Tier 3 (Network): Original images served from CDN edge locations. Access time is 100-500ms depending on connection. Partner-uploaded images go through: upload, server processing (resize, optimize), CDN distribution, client download.
- Below the tiers, write the loading flow sequence: check memory, check disk, fetch from CDN, decode off main thread (CGImageSource), store in memory + disk, display.
Why decode off the main thread?
Image decoding (decompressing JPEG/PNG bytes into bitmap pixels) is CPU-intensive. If you decode on the main thread, the message list stutters every time a new image loads. Moving decode to a background queue keeps the UI at 60fps.
- Add the progressive loading strategy: show a blurred thumbnail placeholder (a tiny ~2KB thumbnail URL included in the message payload), then load the full-resolution image in the background. This gives immediate visual feedback while the full image loads.
- Document memory pressure handling: NSCache evicts automatically. Additionally, subscribe to UIApplication.didReceiveMemoryWarningNotification to clear the memory tier proactively. The disk cache enforces its 200MB budget on write by evicting LRU entries.
Read back your caching section. You should have three tiers with speeds and capacities, the loading flow sequence, progressive loading, and memory pressure handling.
- Now document the build-vs-buy tradeoff. Write the following decision in your notes:
"I would use SDWebImage for image caching because it handles the full pipeline (memory + disk + network + decode) out of the box with TTL, disk budget enforcement, and progressive loading. Building this from scratch adds ~500 lines of infrastructure code and risk without teaching the team anything new."
What should I say in the interview?
Cindy wants to hear that you understand the full pipeline even if you delegate to a library. Describe all three tiers and the loading flow first. Then state the pragmatic decision: "In production, I would use SDWebImage. But I can build it custom if the team prefers zero dependencies."
Under memory pressure, say: "The memory cache self-evicts via NSCache. The disk cache has a hard 200MB budget with LRU eviction. Images are re-fetched from CDN on next access if evicted from both tiers."
Design the AI/ML integration for smart reply suggestions
The job description mentions AI/ML integration. Smart reply suggestions are directly relevant to a messaging feature. They reduce partner response time and improve traveler satisfaction. The design question is where inference runs: on the device or on the server.
- In your notes, add a new section titled "AI/ML Integration" and write the tradeoff comparison below.
On-device ML (Core ML) vs server-side ML:
- On-device: zero latency, works offline, private (user data never leaves device). But limited model size (~50-200MB practical limit), harder to update (requires app update or background model download), less powerful than server models.
- Server-side: access to large models (LLMs), easy to update and A/B test, can use full conversation context. But requires network, adds latency (200-500ms), raises privacy considerations.
- Write the hybrid decision: "A lightweight on-device model (Core ML, ~20MB) generates 2-3 quick reply suggestions based on the last message received. For complex or context-heavy responses, a server-side model uses the full conversation history, streamed back via the existing WebSocket connection."
Why a hybrid approach?
Quick replies ("I'll confirm shortly", "Check-in is at 3 PM") are simple pattern matches that a small model handles instantly. Complex replies that reference reservation details, policies, or multi-turn context need larger models with more data. The hybrid approach gives instant suggestions while the richer ones load.
- Add the content ranking scoring model to your notes. This prioritizes important conversations in the list without a server round-trip.
Document these ranking signals:
- Conversations with check-in within 24 hours rank highest.
- Unread messages rank above read.
- Messages with questions rank above statements.
- Write the decision criteria: "If the inference is simple, latency-sensitive, and privacy-sensitive, use on-device. If it requires large context windows, frequent model updates, or A/B testing, use server-side."
What is MLUpdateTask?
Core ML supports on-device model personalization through MLUpdateTask. This modifies an updatable Core ML model with new labeled data on-device, enabling personalized smart reply suggestions over time without sending user data to servers. If Cindy asks how suggestions improve per-partner, this is your answer.
Review your AI/ML section. You should have the on-device vs server tradeoff, the hybrid decision, the content ranking model, and the decision criteria.
Articulate how you would measure performance and run experiments
A design without measurement is a guess. Cindy will ask how you know the system performs well in production. You need concrete tools and metrics, not vague statements about "monitoring."
- In your notes, add a section titled "Performance Measurement" and document the four monitoring approaches below.
MetricKit:
- Automatic weekly reports on launch time, hang rate, disk writes, and cellular data usage. No code instrumentation needed. Use MXMetricManager to receive payloads. Key metrics for messaging: launch time regression, frame drop rate on message list, network payload sizes.
os_signpost:
- Instrument specific code paths with named intervals. Measure message send-to-display latency, image decode time, and WebSocket reconnect duration. View in Instruments Time Profiler.
Instruments (Xcode):
- Allocations: detect memory leaks from image cache.
- Time Profiler: find CPU hotspots in message rendering.
- Core Animation: catch frame drops and offscreen renders in bubble layout.
Network performance:
- Track API latency percentiles: p50, p95, p99. Track error rates by endpoint and payload sizes. Log to an observability backend (Datadog, Splunk, or custom). Alert on latency spikes.
In plain terms: what are these performance tools?
MetricKit is like a weekly health report card from Apple. It automatically tells you how fast your app launches, how often it freezes, and how much data it uses. No extra code needed.
os_signpost is like placing a start/stop timer around specific operations. You wrap a reconnection attempt or an image decode with markers so you can see exactly how long each one takes.
Instruments is a profiling tool built into Xcode. Think of it as an X-ray for your app: it shows where memory is being used, which code is slow, and where the screen stutters. In the interview, just name which Instrument you would use for which problem.
- Now document the A/B testing measurement strategy for smart replies. Write this in your notes:
- Feature flags gate the smart reply feature (ON/OFF). Users assigned to cohorts server-side.
- Primary metrics: partner response time (did smart replies make partners respond faster?) and conversation resolution rate.
- On-device: log feature flag state + user actions to analytics. Server correlates cohort assignment with outcomes.
- Guard rail: MetricKit confirms no performance regression from the ML inference.
- Write the interview statement: "I would A/B test the smart reply feature by measuring partner response time and conversation completion rate. If partners with smart replies respond 30% faster, we ship it broadly. MetricKit confirms no performance regression from on-device inference."
Connecting measurement to your earlier design
Notice how each monitoring tool maps to a specific part of your architecture. MetricKit covers aggregate app health. The os_signpost intervals cover your critical paths (WebSocket reconnect, image decode). Instruments covers the UI layer (message list scrolling). Network percentiles cover the API layer. In the interview, connect each tool to the component it measures.
Review your full performance measurement section. You should have four monitoring approaches (MetricKit, os_signpost, Instruments, network percentiles) and the A/B testing strategy with specific metrics.
Before you move on, think about this: if Cindy asks "How do you know the messaging feature performs well?", can you name specific tools AND specific metrics for each layer of your architecture?
Notes feeling disorganized?
If your performance section feels scattered, group by layer: UI performance (Instruments Core Animation, frame drops), network performance (percentiles, payload sizes), background performance (MetricKit aggregates), and experimentation (A/B testing). Each group maps to a follow-up question Cindy might ask.
Help me organize my performance notes by architecture layer.
You now have the complete technical depth for Cindy's performance and AI/ML questions. Next up, you will synthesize everything into a timed delivery and practice presenting the full system design end-to-end.
Synthesize: Practice the Full Delivery
You now have every component of the messaging system designed: architecture, WebSocket lifecycle, API contract, security, performance, and AI/ML integration.
Knowing the components is not enough. Cindy evaluates whether you can present a coherent system design under time pressure, handle interruptions gracefully, and pivot to follow-up questions without losing your thread. This step is where you practice the full delivery end-to-end.
Practice the timed delivery
The core presentation (before Q&A) should take about 15 minutes. You need to hit five timed beats in order: requirements, architecture, WebSocket deep-dive, security and performance, and scalability.
- Open your notes document and write the following delivery script outline:
DELIVERY SCRIPT (15 minutes)
Minutes 0-3: Requirements and scope
- 1:1 partner-traveler messaging per reservation
- Real-time via WebSocket, offline queueing, push when backgrounded
- In scope: text, images, typing indicators, delivery status, smart replies
- Out of scope: group chat, video, E2E encryption
Minutes 3-6: High-level architecture
- Draw layered diagram (UI > ViewModel > Service > Repository > Data Sources > Infrastructure)
- Key point: DB is single source of truth, ViewModels read only from DB
- Repository coordinates REST, WebSocket, and local persistence
Minutes 6-10: WebSocket lifecycle deep-dive
- Draw 6-state machine (DISCONNECTED, CONNECTING, CONNECTED, BACKING_OFF, RECONNECTING, DRAINING_QUEUE)
- Background handling: proactive close, silent push, reconnect on foreground
- Backoff: exponential with jitter, capped at 30s
- Delivery guarantee: at-least-once for messages (UUID dedup), at-most-once for typing
Minutes 10-13: Security and performance
- Keychain with kSecAttrAccessibleAfterFirstUnlock
- NSPinnedDomains + delegate-based pinning for WebSocket
- 3-tier image cache: memory (NSCache ~50MB) > disk (200MB/LRU) > CDN
- Progressive loading with thumbnail placeholders
- Smart replies: Core ML on-device for quick, server-side for complex
Minutes 13-15: Scalability and extensions
- Architecture scales by feature domain (each gets own repository + data sources)
- MetricKit for aggregate health, os_signpost for critical paths
- A/B testing for new features like smart replies
Why 15 minutes for the core presentation?
In a 60-minute system design interview, you spend roughly 15 minutes presenting your design. The remaining time goes to answering follow-ups and going deeper where the interviewer probes.
If your core presentation runs longer than 15 minutes, you eat into Q&A time where you demonstrate adaptability.
- Set a 15-minute timer on your phone or computer.
- Switch back to your Excalidraw diagrams from earlier (the high-level architecture and WebSocket state machine).
- Talk through your design out loud, following the 5-beat script above.
- Practice drawing the diagrams as you speak (aim for under 3 minutes per diagram).
After the timer goes off, check which beats you completed and which you rushed or skipped. If you ran out of time before reaching scalability (beat 5), your earlier beats are too detailed. Trim them and try again.
Record yourself
If you have a voice recorder or screen recording tool, record your delivery. Listening back reveals filler words, long pauses, and moments where you lose the thread. These are invisible while you are speaking.
Prepare for follow-up questions Cindy might ask
After your core presentation, Cindy will probe the decisions you made. Having a crisp one-sentence answer ready for each common follow-up shows you have thought through the implications, not just the surface design.
- In your notes, write the following prepared answers for the first five follow-up questions:
FOLLOW-UP ANSWERS (Part 1)
Q: What happens if the WebSocket and REST return the same message?
A: Upsert by message ID in the local DB. Duplicates are deduplicated at the persistence layer.
Q: How do you handle message ordering?
A: Server assigns monotonically increasing sequence IDs. Client renders sorted by sequence ID, not local timestamp.
Q: What if the token expires mid-conversation?
A: WebSocket manager detects the close, transitions to BACKING_OFF, refreshes via refresh_token flow, reconnects with new access_token.
Q: How do you handle large images?
A: Client-side resize before upload (max 1024px width). Server optimizes. CDN serves from edge. Progressive loading shows placeholder.
Q: What about battery drain from the WebSocket?
A: Proactive close on background. Heartbeat interval 25s (below typical LB idle timeout of 60s). Ping timeout 10s.
Why one-sentence answers?
A one-sentence answer demonstrates clarity. You can always expand if Cindy asks for more detail. Starting with a long explanation signals you are unsure which part matters most.
- Continue in your notes with the remaining four follow-up answers:
FOLLOW-UP ANSWERS (Part 2)
Q: Why not GraphQL subscriptions instead of raw WebSocket?
A: If team uses Apollo iOS, subscriptions fit. For new messaging, raw WebSocket avoids dependency and gives full lifecycle control.
Q: How would you A/B test the smart reply feature?
A: Feature flag gates ML inference. Measure partner response time and resolution rate per cohort. MetricKit confirms no performance regression.
Q: What delivery guarantee do you use?
A: At-least-once for chat messages with UUID deduplication. At-most-once for ephemeral events like typing indicators.
Q: How do you handle new messages arriving while user scrolls up through history?
A: New messages append at bottom. Scroll anchored to current reading position. "New messages" badge at bottom. Tap scrolls to latest.
- Read each answer aloud once. If any answer takes more than 10 seconds to say, it is too long. Trim it.
Prepare your questions for Cindy about the team
The interview tips say to come prepared with questions about the team, technology, and culture. Asking specific technical questions shows you have done your homework on the messaging domain.
- Add these three questions to the bottom of your notes:
QUESTIONS FOR CINDY
1. What does the current messaging architecture look like? Is it WebSocket-based or polling-based, and are there pain points the team is actively addressing?
2. How does the team handle feature rollouts for real-time features? Do you use feature flags to gate WebSocket vs polling fallback?
3. What does the on-call rotation look like for the messaging service? How do you monitor message delivery health in production?
What makes these good interview questions?
Each question demonstrates domain knowledge (you know WebSocket vs polling is a real decision). They signal interest in operational maturity (on-call, monitoring) and open a conversation about real problems the team faces.
Generic questions like "what's the team culture?" give the interviewer nothing to work with. Specific technical questions show you are already thinking like a team member.
Before you consider this step complete, ask yourself: can you deliver all five beats from memory in 15 minutes without reading your notes? If not, run the timer again.
- Set your timer to 15 minutes one more time.
- Deliver the full design from memory, drawing your diagrams in Excalidraw as you speak.
- After finishing, pick 3 follow-up questions at random from your list and answer them aloud without looking at your notes.
You should be able to hit all five beats within 15 minutes and answer any follow-up in under 10 seconds. If you stumble on a particular beat or question, that is the area to review in the relevant earlier step.
Running over 15 minutes?
The most common cause is spending too long on requirements (beat 1). Requirements should be a quick list, not a discussion. State them as facts, not explorations.
If you are still over time after trimming beat 1, your architecture explanation (beat 2) likely describes each layer in too much detail. Name the layers and their responsibilities in one sentence each, then move on.
Help me trim my delivery timing.
Your system design delivery is ready. You have a structured 15-minute presentation, crisp follow-up answers, and thoughtful questions for Cindy. If you want an extra challenge, the Secret Mission extends your design into the hardest iOS problem: background message delivery.
Secret mission
Background Delivery and Sync
Cindy may push beyond the core design into what happens when the app is completely suspended. Design the notification delivery and background sync layer using APNs silent push and BGAppRefreshTask to handle messages received while the app is suspended. This is one of the hardest problems in mobile development.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and in your browser. There are no cloud resources, subscriptions, or ongoing costs to worry about.
Resources you used:
- Excalidraw diagrams (high-level architecture, WebSocket state machine, background delivery sequence diagram).
- Notes document (requirements, tradeoffs, API contract, delivery script, follow-up answers).
Keep everything running
No action needed. Choose this if you are still preparing for your interview and want to refine your diagrams or practice your delivery.
- Your Excalidraw diagrams persist in your browser's local storage as long as you do not clear site data.
- Your notes document stays wherever you saved it (local file or notebook).
- There are no running processes or costs associated with keeping these resources.
Pause - I'll come back to this later
Export your work so it survives browser cache clears or device changes.
- In Excalidraw, click the menu icon (top left), then select Export to file to save each diagram as a .excalidraw file on your Desktop.
- Save your notes document to a known location (Desktop or a cloud drive like iCloud or Google Drive).
- You can reopen any .excalidraw file later by dragging it into the Excalidraw browser tab.
Delete - I don't want to use this again
Remove all project artifacts from your machine and browser.
- In Excalidraw, click the menu icon (top left), then select Reset the canvas to clear your diagrams from browser storage.
- If you exported any .excalidraw files to your Desktop, delete them.
- Delete your notes document (or the relevant sections if you keep the file for other purposes).
Nice Work!
Nice Work!
Nice work! You have just designed a complete real-time messaging system for a mobile system design interview, covering architecture, security, performance, and AI/ML integration.
You learned how to:
- Design a WebSocket connection state machine with 6 states handling disconnection, reconnection, iOS background transitions, and token refresh deterministically.
- Define a typed API contract with cursor-based pagination, WebSocket event envelopes, at-least-once delivery guarantees, and articulate REST vs GraphQL tradeoffs with clear rationale.
- Practice a structured 15-minute delivery covering requirements, architecture, deep-dives, and tradeoffs, with prepared answers for follow-up questions on security, performance, and scalability.
- Secret Mission: Designed the background delivery pipeline using APNs silent push + BGAppRefreshTask + local notifications to handle messages received while the app is suspended.
Ready to quiz yourself?