← Back to books

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:

  1. Start with user interactions - What does the user do?
  2. Show data flow - How does data move through the system?
  3. Identify key components - What are the major building blocks?
  4. 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:

  1. Purpose: What problem does it solve?
  2. Interface: How do other components interact with it?
  3. Implementation: How does it work internally?
  4. Edge Cases: What can go wrong?
  5. 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.