Chapter 2: A Universal Framework for Mobile System Design
Introduction
Walking into a system design interview without a framework is like navigating without a map. You might eventually reach your destination, but you'll waste time, miss important landmarks, and appear disorganized.
This chapter presents a six-step framework specifically designed for mobile system design interviews. Unlike generic frameworks, this one addresses mobile-specific concerns at each stage, ensuring you cover what interviewers expect while demonstrating platform expertise.
The Six-Step Framework
┌─────────────────────────────────────────────────────────┐
│ THE FRAMEWORK │
├─────────────────────────────────────────────────────────┤
│ Step 1: Requirements Clarification │
│ ↓ │
│ Step 2: Define Scope and Constraints │
│ ↓ │
│ Step 3: High-Level Architecture │
│ ↓ │
│ Step 4: Data Layer Design │
│ ↓ │
│ Step 5: Component Deep Dive │
│ ↓ │
│ Step 6: Trade-offs and Optimizations │
└─────────────────────────────────────────────────────────┘
Step 1: Requirements Clarification (5-7 minutes)
Never assume you understand the problem. Interviewers intentionally leave requirements ambiguous to test your ability to gather information.
Functional Requirements
Ask about core features:
- What are the primary user actions?
- What are the key screens or flows?
- Are there different user roles?
- What features are must-have versus nice-to-have?
Example questions for a chat app:
- "Is this 1:1 messaging, group messaging, or both?"
- "Do we need to support media messages like images and videos?"
- "Are read receipts and typing indicators required?"
- "Is message search a core feature?"
Non-Functional Requirements
These often differentiate good designs from great ones:
| Category | Questions to Ask |
|---|---|
| Scale | How many users? Messages per day? |
| Performance | What latency is acceptable? |
| Offline | Must the app work offline? |
| Security | What data is sensitive? Compliance requirements? |
| Platform | Android only, iOS only, or both? |
Documenting Requirements
Write down what you learn. This demonstrates organization and gives you reference points throughout the interview.
Functional Requirements:
- 1:1 and group chat (up to 50 members)
- Text and image messages
- Read receipts and typing indicators
- Message history with search
Non-Functional Requirements:
- 10M daily active users
- Messages delivered within 500ms
- Offline message composition
- End-to-end encryption for 1:1 chats
- Android and iOS
Step 2: Define Scope and Constraints (3-5 minutes)
You cannot design everything in 45 minutes. Explicitly define what you will and won't cover.
Scoping Technique
Propose a scope and get interviewer buy-in:
"Given the time, I'll focus on the core messaging flow including offline support and sync. I'll touch on group chat architecture but won't deep-dive into media upload optimization. Does that align with what you'd like to explore?"
This shows maturity and time management awareness.
Mobile-Specific Constraints to Consider
Always mention these constraints as they apply:
Device Constraints
- Memory limits (especially on low-end Android devices)
- Storage availability
- Battery consumption
Network Constraints
- Intermittent connectivity
- Variable bandwidth
- High latency on cellular networks
Platform Constraints
- Background execution limits (iOS especially)
- Permission requirements
- App Store / Play Store guidelines
Define Success Metrics
What makes this design successful?
Success Metrics:
- Messages sync within 2 seconds of connectivity restoration
- App cold start under 1 second
- Memory footprint under 100MB
- Battery drain under 2% per hour of active use
Step 3: High-Level Architecture (10 minutes)
Now you design. Start with the big picture before diving into details.
The Three-Layer Mobile Architecture
Most mobile apps follow a layered architecture:
┌─────────────────────────────────────────┐
│ Presentation Layer │
│ (UI, ViewModels, State Management) │
├─────────────────────────────────────────┤
│ Domain Layer │
│ (Business Logic, Use Cases) │
├─────────────────────────────────────────┤
│ Data Layer │
│ (Repositories, Data Sources, Cache) │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ External Services │
│ (APIs, Databases, Platform) │
└─────────────────────────────────────────┘
Drawing the Architecture
When whiteboarding or diagramming:
- Start with user interactions - What does the user do?
- Show data flow - How does data move through the system?
- Identify key components - What are the major building blocks?
- Mark platform boundaries - Where do we interact with OS/network?
Platform-Specific Architectural Patterns
Android (Modern)
UI Layer: Jetpack Compose + ViewModel
Domain Layer: Use Cases / Interactors
Data Layer: Repository + Room + Retrofit
DI: Hilt
Async: Kotlin Coroutines + Flow
iOS (Modern)
UI Layer: SwiftUI + ObservableObject
Domain Layer: Use Cases / Services
Data Layer: Repository + Core Data/SwiftData + URLSession
DI: Manual or Swinject
Async: Swift Concurrency (async/await)
Component Diagram Example
For a chat application:
┌────────────────────────────────────────────────────────────┐
│ UI LAYER │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Conversation │ │ Message │ │ Chat │ │
│ │ List │ │ Composer │ │ Screen │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ ↓ │
│ ┌───────────────┐ │
│ │ ViewModel │ │
│ └───────┬───────┘ │
└───────────────────────────┼────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────┐
│ DOMAIN LAYER │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Send │ │ Sync │ │ Search │ │
│ │ Message │ │ Messages │ │ Messages │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
└─────────┼────────────────┼────────────────┼───────────────┘
└────────────────┼────────────────┘
↓
┌───────────────────────────────────────────────────────────┐
│ DATA LAYER │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Repository │ │
│ └───────────────────────┬─────────────────────────────┘ │
│ ┌───────────┴───────────┐ │
│ ↓ ↓ │
│ ┌───────────────────┐ ┌───────────────────┐ │
│ │ Local Source │ │ Remote Source │ │
│ │ (Room/CoreData) │ │ (WebSocket/API) │ │
│ └───────────────────┘ └───────────────────┘ │
└───────────────────────────────────────────────────────────┘
Step 4: Data Layer Design (8-10 minutes)
The data layer is where mobile system design gets interesting. This is where you handle offline support, caching, and synchronization.
Data Models
Define your core entities:
// Android (Kotlin)
@Entity(tableName = "messages")
data class Message(
@PrimaryKey val id: String,
val conversationId: String,
val senderId: String,
val content: String,
val timestamp: Long,
val status: MessageStatus,
val localId: String? = null // For optimistic updates
)
enum class MessageStatus {
SENDING, SENT, DELIVERED, READ, FAILED
}
// iOS (Swift)
@Model
class Message {
@Attribute(.unique) var id: String
var conversationId: String
var senderId: String
var content: String
var timestamp: Date
var status: MessageStatus
var localId: String?
init(id: String, conversationId: String, senderId: String,
content: String, timestamp: Date, status: MessageStatus) {
self.id = id
self.conversationId = conversationId
self.senderId = senderId
self.content = content
self.timestamp = timestamp
self.status = status
}
}
Repository Pattern
The repository abstracts data sources from the rest of the app:
┌─────────────────────────────────────────────────────────┐
│ Repository │
│ ┌───────────────────────────────────────────────────┐ │
│ │ - getMessages(): Flow<List<Message>> │ │
│ │ - sendMessage(message): Result<Message> │ │
│ │ - syncMessages(): Result<Unit> │ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ↓ ↓ ↓ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │Local Source │ │Remote Source│ │ Cache │ │
│ │(Database) │ │(API/Socket) │ │ (Memory) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
Offline-First Strategy
Design for offline as the default, not an exception:
┌─────────────────────────────────────────────────────────┐
│ OFFLINE-FIRST DATA FLOW │
├─────────────────────────────────────────────────────────┤
│ │
│ User Action │
│ ↓ │
│ Write to Local DB (immediate) │
│ ↓ │
│ Update UI (optimistic) │
│ ↓ │
│ Queue for Sync │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ Network Available? │ │
│ └──────────┬──────────┘ │
│ Yes │ No │
│ ↓ │
│ ┌──────────────────────┐ ┌────────────────────┐ │
│ │ Sync to Server │ │ Keep in Queue │ │
│ │ Update Local Status │ │ Retry on Connect │ │
│ └──────────────────────┘ └────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
Caching Strategies
| Strategy | Use Case | Implementation |
|---|---|---|
| Cache-First | Read-heavy, staleness OK | Check cache → return if valid → fetch in background |
| Network-First | Freshness critical | Fetch from network → fallback to cache on failure |
| Stale-While-Revalidate | Balance of speed and freshness | Return cache immediately → update from network |
Step 5: Component Deep Dive (10 minutes)
Pick 1-2 critical components and design them in detail. This is where you demonstrate depth.
Choosing What to Deep Dive
Select components that are:
- Core to the user experience
- Technically interesting
- Relevant to the specific question
For a chat app, good choices include:
- Message synchronization mechanism
- Real-time delivery pipeline
- Offline queue management
Deep Dive Structure
For each component:
- Purpose: What problem does it solve?
- Interface: How do other components interact with it?
- Implementation: How does it work internally?
- Edge Cases: What can go wrong?
- Platform Specifics: Any Android/iOS differences?
Example Deep Dive: Message Sync Manager
Purpose: Ensure messages are synchronized between local storage and server, handling conflicts and failures gracefully.
Interface:
interface MessageSyncManager {
suspend fun syncConversation(conversationId: String): SyncResult
suspend fun syncAll(): SyncResult
fun observeSyncStatus(): Flow<SyncStatus>
suspend fun retryFailed()
}
Implementation:
┌─────────────────────────────────────────────────────────┐
│ MESSAGE SYNC FLOW │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. Get local pending messages │
│ ↓ │
│ 2. Get server timestamp of last sync │
│ ↓ │
│ 3. Fetch server messages since last sync │
│ ↓ │
│ 4. Merge and resolve conflicts │
│ │ │
│ ├─→ Server wins: Update local │
│ ├─→ Client wins: Push to server │
│ └─→ Conflict: Apply resolution strategy │
│ ↓ │
│ 5. Upload pending local messages │
│ ↓ │
│ 6. Update sync timestamp │
│ ↓ │
│ 7. Notify observers │
│ │
└─────────────────────────────────────────────────────────┘
Edge Cases:
- Partial sync failure (some messages uploaded, some failed)
- Conflict between offline edit and server edit
- Network timeout mid-sync
- App killed during sync
Platform Specifics:
- Android: Use WorkManager for background sync with constraints
- iOS: Use BGTaskScheduler with BGAppRefreshTask
Step 6: Trade-offs and Optimizations (5 minutes)
End strong by discussing trade-offs and potential optimizations.
Trade-off Framework
For each major decision, articulate:
- What you chose
- What you gave up
- Why the trade-off makes sense
Example:
"I chose SQLite over Realm for the local database. SQLite with Room/Core Data gives us better query flexibility and is more widely understood by mobile developers. The trade-off is that Realm would give us automatic sync capabilities and reactive queries out of the box. Given our custom sync requirements and the team's familiarity with SQL, SQLite is the better choice."
Common Mobile Trade-offs
| Decision | Option A | Option B |
|---|---|---|
| Sync Strategy | Real-time (WebSocket) | Polling |
| Lower latency, battery drain | Battery efficient, higher latency | |
| Storage | SQLite | NoSQL |
| Query flexibility, schema required | Schema flexibility, limited queries | |
| Images | Download on demand | Prefetch aggressively |
| Less bandwidth, slower display | Faster display, more bandwidth | |
| State | Single source of truth | Distributed state |
| Consistency, complexity | Simplicity, potential inconsistency |
Optimization Opportunities
Mention optimizations you'd consider with more time:
- Pagination: Implement cursor-based pagination for large lists
- Compression: Compress payloads for slow networks
- Prefetching: Predict user navigation and prefetch data
- Lazy Loading: Load heavy resources on demand
- Memory Management: Implement LRU caches with size limits
- Battery: Batch network requests, use appropriate sync intervals
Framework Summary Checklist
Use this checklist to ensure you cover everything:
□ Step 1: Requirements
□ Functional requirements clarified
□ Non-functional requirements clarified
□ Requirements documented
□ Step 2: Scope
□ Scope explicitly defined
□ Mobile constraints identified
□ Success metrics defined
□ Step 3: High-Level Architecture
□ Layer diagram drawn
□ Key components identified
□ Data flow explained
□ Platform patterns mentioned
□ Step 4: Data Layer
□ Data models defined
□ Repository pattern explained
□ Offline strategy described
□ Caching approach chosen
□ Step 5: Deep Dive
□ 1-2 components selected
□ Detailed design provided
□ Edge cases addressed
□ Platform specifics covered
□ Step 6: Trade-offs
□ Key trade-offs articulated
□ Optimizations mentioned
□ Future improvements suggested
Summary
This framework provides a reliable structure for any mobile system design question. It ensures you:
- Gather requirements before designing
- Explicitly manage scope and time
- Cover both breadth (high-level architecture) and depth (component deep dive)
- Address mobile-specific concerns throughout
- Demonstrate seniority through trade-off discussions
Practice this framework until it becomes second nature. In the following chapters, we'll apply it to real mobile system design problems, starting with designing a social feed application.