Study Notes
Note

Mobile System Design Building Blocks

The architecture, storage, networking and delivery patterns you reach for again and again — and the trade-offs that decide between them.

35 min read
Mobile System Design · Chapter 10

A working vocabulary for mobile interviews — the architecture, storage, networking, and delivery patterns you reach for again and again, and the trade-offs that decide between them.

📖 How to read this

Each section defines a building block, says when to raise it in an interview, and names the trade-off that picks between options. A [+] badge marks context added beyond the source. Diagrams are original. Every further-reading link was fetched and confirmed live on 24 August 2026.

01

Good architecture is a shared language, not a diagram#

Architecture does two jobs at once: it gives an app a structure that stays maintainable as it grows, and it gives a team a common vocabulary for talking about that structure. In interviews you rarely have to name a framework — you have to show you understand the principles the frameworks encode. A handful of principles carry almost every answer.

Separation of concerns#

Keep UI logic out of everything else. A UI class should only display content and react to user input; business rules and data access live elsewhere. This makes each piece testable in isolation and sidesteps a whole category of bugs tied to the UI lifecycle, because the logic no longer dies and resurrects with the screen.

Unidirectional Data Flow#

Data moves in one direction — from a source (say a database) through transformation layers to the UI. Changes travel back the other way as events the source exposes; nothing mutates data by reaching sideways into it. That single rule buys predictability (changes only happen through defined paths), fewer bugs (there are fewer places data can change), and traceability (you can follow exactly how a value reached the screen).

Source owns the data Business logic transformations Consumer UI state events (request changes)
Figure 1 — State flows one way to the UI; events flow back to the source that owns the data.

Reactive programming is the usual way to implement this flow: the source publishes a stream, consumers subscribe, and updates propagate automatically. Kotlin Flows or RxJava on Android; Combine or RxSwift on iOS; Kotlin suspend functions and Swift async/await for one-off async work. Callbacks and event buses can work for narrow cases and event buses do reduce coupling, but streams keep state management far easier to reason about.

Immutable data and a single source of truth#

Immutable state — data that can't change after creation — removes a class of concurrency bugs and makes behavior predictable, which matters on mobile where several threads often touch the same data. Reserve mutable structures for rare performance-critical spots (an image-processing module, say) and isolate them from the app's main state.

Pair immutability with a Single Source of Truth (SSOT): each type of data has exactly one owner with exclusive authority to change it. The owner receives change events, applies business rules, and re-exposes updated immutable state. A local database is often the SSOT for offline business data; a UI state holder is the SSOT for its screen's display state.

Dependency Injection#

As components multiply, so do the dependencies between them. Prefer Dependency Injection over the Service Locator pattern: rather than a component constructing what it needs, its dependencies are handed in — usually through the constructor. That makes requirements explicit and lets you swap real implementations for test doubles. Service Locator can hide dependencies and complicate testing, which is why this book avoids it.

↳ Reading the diagrams

Throughout the book, arrows show data flow, not dependencies. Dependency Injection appears as a box sitting between architectural layers — a boundary that keeps components loosely coupled.

Decoupling, error propagation, and minimal exposure#

  • Decouple through interfaces. Depend on an abstraction, not a concrete class, so a MapsRepository can talk to a MapsProvider interface — swapping a test double in tests and A/B-testing an in-house vs. third-party provider in production.
  • Propagate errors deliberately. The UI shows friendly messages and handles simple validation; lower layers handle business-logic and data-source failures and pass up only what the user needs to know. Kotlin coroutines and Rx operators (onError ) handle this natively; on platforms without such tools, a Result type that wraps success/failure models the cases explicitly.
  • Expose the minimum. Each component receives only the data it needs. A conversation-list screen wants names, message previews, and timestamps — not every message in every thread. Less data means less memory, CPU, bandwidth, and battery, and simpler flows to test. Transform data at layer boundaries so each layer handles only what's relevant to it.
02

Two layers, sometimes three, and a repository in the middle#

The reference architecture here follows Google's guide to app architecture. Written for Android, its principles transfer cleanly to iOS. At its core sit two layers — UI and data — with an optional domain layer between them for logic that's shared or reused across screens.

Data layer data + business logic Domain layer optional UI layer what the user sees
Figure 2 — Data flows from storage, through optional shared logic, to presentation.

UI layer#

The UI layer (also called the presentation layer) renders application data and is the primary point of interaction. It has two parts: UI elements that draw to the screen, and state holders that hold data, expose it, and run presentation logic. The state holder turns business data into UI state — a blueprint of what should be on screen. As the docs put it: if the UI is what the user sees, the UI state is what the app says users should see. Each screen gets its own state holder, wired in through dependency injection. Whether this reads as MVVM or MVI is beside the point — both express Unidirectional Data Flow, and the interview should stay on the overall design rather than the label.

Data layer#

The data layer implements the repository pattern. A repository per data type exposes data to the app, centralizes changes, resolves conflicts between sources, hides where data comes from, and holds business logic. Under each repository sit data sources, each responsible for exactly one source — a network endpoint, a local database, or a file. A remote data source talks to the backend; a local data source persists to the device, often via a database. Define repositories as interfaces so DI can swap the real implementation for a test double.

For most interviews, simplify navigation to a global Navigator (or Coordinator) that moves between screens and supplies each screen's dependencies — both static ones like state holders and platform services, and dynamic ones like the item ID a detail screen needs. Real apps get far messier as screens, dialogs, and deep links [+] multiply, but you rarely need to design a full navigation graph unless asked.

🔍 In the wild

Duolingo moved to MVVM with Jetpack and Dagger/Hilt for DI. Notion leans on a flexible data model and unidirectional flow, which brought power and complexity to its mobile clients. If you've shipped a different architecture successfully, say so — lived experience is as valuable as any reference model.

03

Modules for build speed, tests for trust#

Modularization splits an app into independent building blocks. It aids maintainability and scaling, but its headline benefit is build speed: better use of build resources, less code to recompile, and parallel compilation. It isn't free — more modules mean more overhead if you're careless — so it needs planning.

The industry increasingly uses the contract module pattern: each component has at least an API module (the interface) and an implementation module (the code behind it). Features depend only on the stable API module, so changing an implementation doesn't force dependents to rebuild. The top-level app module wires real implementations in; a test module swaps test doubles in.

app module feature module data-layer API interface data-layer impl real code test module implements API test impl
Figure 3 — Features depend on stable API modules; the app module injects the real implementation, the test module a fake.
🔍 In the wild

Airbnb restructured its iOS app into nearly 1,500 modules for team productivity. Robinhood broke up a "giant monolith" Android module into feature modules and cut build times.

A testing strategy worth stating out loud#

Testing shows that your architecture supports quality. Define your terms — teams disagree on names — then cover the pyramid:

Unit tests Components in isolation with test doubles. Fast, reliable, the base of the pyramid.
Integration / navigation tests Multiple components with some real implementations. Broader coverage, slower, more upkeep.
Screenshot tests Capture a reference image of UI and compare against changes to catch visual regressions.
End-to-end tests Real backend and dependencies. The most thorough and the most flaky — keep few, on critical journeys.

Layer in specialized checks where they matter — accessibility, performance, memory-leak, and security tests [+] cut across the pyramid rather than replacing it. Fake the backend with OHHTTPStubs on iOS or MockWebServer on Android, and lean on the robot pattern to keep UI tests readable.

04

Pick storage by data shape, not by habit#

How an app persists data drives its performance and reliability. Start by naming what kind of data you're holding — each has a different lifetime and access pattern:

  • User data — profile info, settings, preferences that personalize the experience.
  • User files — photos, videos, documents the user creates; lost if the user deletes them.
  • Business data — articles, listings, messages; re-fetchable from the backend if removed.
  • Sensitive information — tokens, encryption keys; needs protected storage.
  • Cache data — downloaded images, precomputed results; in memory or on disk.
  • Temporary data — upload chunks, drafts, undo history; safely discarded when done.

The five options#

Key-value Dictionary-style primitives. Simple, fast for small sets; no querying, relationships, or strong security. Android: SharedPreferences, DataStore. iOS: UserDefaults, Plists.
Secure storage Hardware-backed encryption for credentials and keys. More complex, limited capacity, some overhead. Android: Keystore. iOS: Keychain (+ iCloud Keychain sync).
Relational DB Large datasets, complex queries, relationships, integrity, migrations. Heavier setup and resources. SQLite via Room (Android) or Core Data (iOS).
Non-relational DB Schema-less, fast reads/writes, real-time sync. Weak on complex queries and ACID guarantees. Realm, Firebase Realtime Database.
Files & directories Custom formats and binary data with simple I/O. You manage integrity, querying, and concurrency yourself. iOS FileManager, Android File APIs.
MechanismBest forWatch out for
Key-valueNon-sensitive preferences, settings, feature togglesLarge or structured data; no advanced queries; weaker security
Secure storageCredentials, payment data, cryptographic keysEncryption overhead; limited capacity; harder to test
Relational DBLarge structured datasets with relationships and complex queriesSetup and maintenance cost; resource use; query latency
Non-relational DBUnstructured or semi-structured data, high-throughput CRUDLimited complex queries; potential consistency gaps
FilesBinary blobs, note-per-file designs, custom formatsManual integrity, concurrency, and scaling
Table 1 — Storage options at a glance.

A decision path you can narrate#

Security-critical? Secure storage yes Relationships / rich queries? no Rigid, well-defined schema? yes Relational DB yes Non-relational DB no Mainly binary? else key-value pairs no Files / key-value
Figure 4 — Walk the tree aloud in an interview: security first, then relationships, then structure, then shape.
📌 Real apps mix storage

The tree simplifies. A social app might use secure storage for tokens, a relational DB for profiles and posts, key-value for settings, and files for cached media — all at once. Netflix chose Realm for offline video metadata; Trello made its local DB the single source of truth to work offline.

05

Everything is a file — behind a sandbox wall#

Whatever the API, storage ultimately lands as files on disk; the options differ in how they organize and expose those files. By default, each app gets a private, sandboxed space no other app can read — basic isolation and security. That wall has a limit: on jailbroken iOS or rooted Android, the sandbox can be bypassed, which is exactly why hardware-backed secure storage matters for sensitive data even on compromised devices.

Reaching outside the sandbox#

  • Device-wide storage. Android exposes shared areas — built-in "external" storage and removable media — for files meant to be shared, like photos and documents. iOS is more controlled: a Documents directory for user-visible, iCloud-backed files, and a Library directory for data the app needs but users shouldn't see directly, browsable through the Files app.
  • Media and files from other apps. iOS uses PhotoKit (PHAsset/PHImageManager) to read the photo library and UIDocumentPickerViewController to pick arbitrary files; Android uses MediaStore for media and the Storage Access Framework for documents. These APIs run the permission prompts, keeping the user in control of what each app can touch.
  • Sharing between your own apps. Android uses Content Providers with signature-based permissions so only apps signed with the same certificate share data; iOS uses App Groups, shared containers for apps on the same development team, which can share UserDefaults and files. Both give fine-grained control while blocking other developers' apps.
06

HTTP conventions that keep an API predictable#

Most mobile-to-backend traffic runs over HTTP: universally supported, stateless (so servers don't track sessions), persistent-connection capable in HTTP/1.1 and HTTP/2, format-agnostic, header-driven, and secured by HTTPS. Every request carries four things — a verb, a resource path, headers (auth tokens, expected formats), and optional data in the body or as query parameters.

                    
Accept: application/json
Authorization: Bearer <token>

GET https://my-backend.com/v1/news?after=timestamp&limit=30
                    
                

Query parameters are how GET requests filter, sort, and paginate without a body — and they keep the call idempotent, meaning repeated calls don't change server state.

REST conventions worth following#

REST layers standard practice on top of HTTP. The verbs map to CRUD, and consistency is what makes an API easy to consume:

  • GET read · POST create · PUT update · DELETE remove
  • Give each resource a unique URI — GET /users, GET /users/1.
  • Use query params for filtering/sorting/paging — GET /users?role=admin&sort=lastname_asc&page=2&limit=30.
  • Name resources with plural nouns — GET /posts/{postId}, not /query-posts or singular /post.
  • Nest related resources — GET /v1/posts/{postId}/attachments/{attachmentId}.
  • Version globally — GET /v1/posts/{postId}/attachments, not /posts/{postId}/v1/attachments.

Lean on the standard status codes so clients can react correctly:

200 OK · 201 Created Success; 201 specifically for a POST that created a resource.
400 Bad Request Client error — invalid syntax or missing data.
403 Forbidden · 404 Not Found Authenticated but not allowed; or the resource doesn't exist.
500 Internal Server Error Something failed on the server.

Choosing an encoding format#

HTTP doesn't care about the payload format — client and server just agree via Content-Type and Accept. JSON dominates modern REST because it's readable, compact relative to XML, and parses with standard libraries everywhere.

FormatStrengthsCostsUse when
JSONHuman-readable, universal, compact vs. XMLSlower/larger than binary; no built-in schemaMost web and mobile APIs
XMLRich structure, metadata, XSD validationVerbose, larger, slower to parseEnterprise, document-centric data
Protocol BuffersCompact binary, fast, strict schema, versionedNot readable; needs .proto files and codegenPerformance-critical, large-scale transmission
Table 2 — Encoding trade-offs.
🔍 In the wild

Lyft moved from ad-hoc REST+JSON to an IDL built on Protocol Buffers, handling both gRPC and legacy HTTPS+JSON. Reddit and Airbnb migrated to GraphQL to serve mobile clients better. Snapchat adopted QUIC to speed up mobile networking.

07

Prove who you are, cheaply and statelessly#

Any app with personalized or sensitive data needs to verify users before granting access. Token-based authentication is the mobile default: stateless (no server-side session store, so it scales), cross-platform, capable of embedding permissions and expiry, and light on the backend since validation doesn't hit the database each time. The usual vehicle is a JSON Web Token (JWT) — a compact, self-contained token carrying claims as JSON.

Login screen credentials Auth service validates Identity provider JWKS · optional access + refresh Secure storage API gateway verifies JWT 1 2 3 4 · Bearer <access_token> on each request
Figure 5 — Credentials in, two tokens out; the short-lived access token rides every request while the refresh token renews it.
  1. Login. The client sends credentials to the auth service, which validates them.
  2. Token issue. On success the backend returns an access token (JWT, short-lived — 15–30 min, carrying identity and permissions) and a longer-lived refresh token.
  3. Secure store. Both go into platform secure storage — Android Keystore-backed encryption before SharedPreferences/DataStore, or iOS Keychain protection classes that keep them encrypted and only readable when the device is unlocked.
  4. Authenticated requests. The app attaches Authorization: Bearer <access_token>; an API gateway verifies the JWT (often via the identity provider's JWKS public keys) before forwarding.

Renewal: when the access token expires — detected proactively or on a 401 — the app sends the refresh token to a renewal endpoint, receives a fresh access token (sometimes a new refresh token too), stores it, and retries the original request. If the refresh token is invalid, log the user out.

↑ Hardening beyond the basics

Layer on multi-factor and biometric auth, certificate pinning against man-in-the-middle attacks, a trusted-device registry, device fingerprinting, new-device alerts, and suspicious-activity detection as the data's sensitivity warrants. Robinhood marks devices "trusted" after MFA so repeat logins skip the second factor — security balanced with convenience.

📝 Offload key management

An external Identity Provider can issue and validate tokens and expose a JWKS endpoint, letting your systems verify JWTs with public keys without ever handling private signing keys.

08

Serve heavy, stable content from close by#

A Content Delivery Network spreads servers across regions so static content is served from near the user rather than from the origin backend. In mobile apps it shines for content that's bandwidth-heavy but changes rarely: images and icons, video, HTML, and bundled assets. Every request the CDN serves is one the backend doesn't — that's the whole point.

Client CDN edge caches static Backend origin https pull on miss direct fallback
Figure 6 — The edge absorbs most reads; the origin fills the cache and serves as a fallback.
UpsideDownside
Lower latency and faster loads from nearby edgesCost grows with usage
Absorbs volume spikes without bottlenecksConfiguration, cache management, client integration
Redundancy and easy scalingYour uptime now partly depends on the provider
Offloads static content from the originKeeping content fresh needs careful cache control
Table 3 — CDN trade-offs. Worth it for any app with a worldwide user base.
09

Never send the whole list#

Any time a design shows a list, ask two questions: is there a natural cap on how many items to show at once, and would sending the full set strain the network or device? If the full set is expensive, paginate — load in batches to protect performance, scalability, and memory. Two approaches dominate.

Offset page 3 skip 60 rows, then read → slows as offset grows Cursor cursor read forward from last-seen item → constant cost
Figure 7 — Offset skips rows and degrades with depth; a cursor bookmarks a position and reads onward.

Offset pagination#

Request a page and size — GET /v1/feed?page=3&limit=30 — and the backend skips (page-1)×limit rows and returns the next batch, usually with total_pages /total_items metadata. It's simple, maps to SQL, and lets users jump to any page. But it degrades as the dataset grows (counting and skipping rows gets costly) and misbehaves on fast-changing data — items shift between pages, causing duplicates or gaps. Best for moderate, stable datasets with simple navigation: educational content, e-commerce catalogs, forums.

Cursor-based pagination#

Think of a bookmark. The client sends a cursor for its current position — GET /v1/feed?after=<cursor>&limit=30 — and the response returns data plus next_cursor /prev_cursor. The cursor maps to a stable ordering key (a primary key or timestamp), often Base64-encoded to survive URLs. It handles frequently changing data well, allows a different page size per call, and keeps the backend stateless. The catch: no jumping to an arbitrary page — you traverse sequentially. Meta and Slack use it for their APIs.

Choosing the limit#

The right batch size depends on content weight — light content tolerates higher limits, heavy content needs lower ones:

10–20 items Fast responses, minimal transfer; more frequent fetches and loading indicators.
30–50 items The usual sweet spot — balances payload against fetch frequency.
50+ items Smoother scrolling for stable feeds; slow on weak networks or old devices.

Adaptive pagination [+] goes further, tuning the limit to network conditions or behavior — a larger limit for fast scrollers, a smaller one for lingering readers.

MethodStrengthsLimitsUse for
OffsetSimple, jump to any page, maps to SQLSlows on big data; duplicates on churnModerate stable sets: catalogs, forums
CursorScales, stable under churn, flexible limit, statelessNo random page access; may need encodingReal-time large feeds, event logs
Table 4 — Pagination trade-offs. Decision path: dynamic data → cursor; need random page access → offset; otherwise favor cursor for scale.
🔍 In the wild

Slack migrated from offset to cursor pagination for consistent retrieval at scale. Discord virtualizes infinite lists, cutting startup memory 14% for heavy users. Shopify uses relative cursors keyed on the last-seen item ID.

10

Measure first, then optimize#

Performance drives satisfaction, retention, and revenue — Google even downranks slow apps in the Play Store, and crashes, heavy data use, battery drain, and UI jank are top reasons people uninstall. Raise it when discussing data-heavy features, animations, background work, or low-end devices.

Cover the whole stack#

UI responsiveness Keep the main thread free for interaction; push heavy work to background threads.
Network efficiency Minimize latency and data consumption.
Memory management Prevent crashes and enable smooth multitasking.
Battery Location, continuous network activity, and wake locks are the expensive culprits.

Ground it in data#

Optimize against evidence, not hunches: capture baseline metrics, run continuous monitoring in production across real devices and networks, add targeted instrumentation on critical paths, and validate incrementally after each change. Small, measured improvements beat large speculative refactors. In interviews, be ready to explain how a background component — a prefetcher, a location feature — keeps its resource use in check.

🔍 In the wild

Robinhood used Server-Driven UI to speed feature delivery. Spotify batched metadata downloads with precomputed sorting to cut startup and view-load times. Lyft cut ANRs 21% by optimizing disk reads and adding memory caching.

11

Getting features to users without breaking them#

Solid architecture is the foundation; deployment strategy is how innovations reach users safely. These operational topics rarely dominate an interview, but raising them signals you think past implementation to the whole product lifecycle.

Gradual release#

Roll new versions out to a slice of users first, so problems surface while they're still small. Google Play offers staged rollouts with fine control — start at 1% and climb — plus the ability to halt and revert affected users. Apple's App Store uses a fixed 7-day phased release and, critically, has no rollback: once a user updates, they stay until the next update. Before a wide release, both platforms offer beta programs (TestFlight, Play beta), internal testing tracks, and employee dogfooding. Pair rollouts with strong monitoring and clear severity thresholds — e.g. pause if crash rate rises above 1%.

Force upgrading#

Sometimes old versions must be retired. Force upgrading prompts outdated clients to update before continuing. It adds friction, so justify it: backend-compatibility breaks, security vulnerabilities, regulatory requirements, or analytics consistency. It exists on a spectrum:

  • Hard upgrade — blocks all use until updated; a modal to the app store. For critical security or backend incompatibility.
  • Soft upgrade — a dismissible prompt with persistent reminders; less disruptive, slower to migrate users.
  • Tiered grace periods [+] — start soft, harden to required after a set window.

Mechanically: on launch the client sends its version to a version-check endpoint, the backend decides whether an upgrade is required and of what kind, and the client shows the matching UI. Mind poor connectivity (offline grace periods), old unsupported devices, and staged enforcement so you don't overwhelm app-store or support channels. Uber built a force-upgrade process to manage scaling.

Feature flags, remote config, and A/B testing#

These separate feature availability from code deployment. On launch, the app asks the backend which features to switch on, based on user traits, device capabilities, environment, or experiment cohort — managed by product teams through a CMS without engineering per change. Because the backend controls which variant a user sees, the same machinery powers A/B tests: track metrics per variant and decide with data. Uses include risk containment, staged rollouts, performance experiments, and traffic-shifting during backend migrations. Costs: branching complexity, technical debt from stale flags, combinatorial testing, and murkier UX understanding — so clean flags up after launch.

🔍 In the wild

Netflix runs A/B tests on nearly every change through its "ABlaze" experimentation platform. Duolingo builds an experimentation framework right into the app and credits much of its product success to it.

Observability, analytics, and monitoring#

A complete view spans technical performance monitoring, user-behavior analytics, error tracking, and business metrics. Classify errors deliberately — fatal (crash), non-fatal (feature breaks, app survives), and degraded (works but slower) — and instrument the last two explicitly, since crash reporters only catch the first. Design for low overhead: sampling, batching and compression, offline buffering so nothing is lost without connectivity, and automatic context (device, app version, user segment) for multidimensional analysis. Analytics usually shows up as a cross-cutting repository in the architecture.

🔍 In the wild

Pinterest built a schemaless end-to-end JSON logging system for its clients. Slack's SlackTrace standardized notification-flow tracing to simplify debugging. Signal collects virtually no analytics — no typical mobile analytics or crash SDKs — to protect privacy.

Localization#

Localization is far more than translation — it's number/currency and date/time formatting, pluralization rules, right-to-left layouts (Arabic, Hebrew, Persian), and culturally aware colors and imagery. iOS has built-in translation export; Android localizes through its resource system. Plan for text expansion (German and Finnish often need 30–40% more space than English; Chinese less), dynamic language switching without a restart, asset-size growth per language, exploding test matrices, and backend responses that honor a language preference. Treat it as architecture, not an afterthought.

🔍 In the wild

Netflix, in 190+ countries, built a pipeline to ingest, translate, and serve UI strings — and uses pseudo-localization (elongated, accented text) as a test step to prove layouts survive other scripts.

Privacy#

Privacy is now a requirement, not a feature. Apps often span jurisdictions with distinct frameworks — the EU's GDPR, the UK's DPA, US HIPAA for health data, Australia's APPs — and the landscape keeps shifting. Protect data across its lifecycle: data minimization, secure transit, protected storage (encrypt sensitive data with AES-256 and manage keys well), access controls, anonymization/pseudonymization, and regular security testing. Track the annually updated OWASP mobile risks list.

🔍 In the wild

Meta has explored Federated Learning with Differential Privacy to improve products without exposing user data.

Accessibility#

Inclusive design widens reach and is often legally required (government, education, healthcare). Follow WCAG, Apple's Human Interface Guidelines, and Material Design accessibility guidance. Support screen readers (VoiceOver, TalkBack), dynamic text sizing, sufficient color contrast, and large touch targets. Test with automated tools (Accessibility Scanner, Accessibility Inspector), manual assistive-tech walkthroughs, and — most valuable — real users with disabilities. The BBC's Mobile Accessibility Guidelines are a strong public reference.

Push notifications#

Push reaches users when the app is closed — via APNs on iOS and FCM on Android, both acting as intermediaries between your backend and devices. The flow: the app registers with the platform service, receives a device token, sends it to the backend; when needed, the backend hands a message to the platform service, which attempts delivery.

⚠️ Delivery is best-effort

Offline queuing that eventually drops, battery-saver delays, platform throttling, payload-size caps, manufacturer background-handling quirks, and user permission or Do-Not-Disturb settings all block delivery. Treat push as a supplementary channel — always have another path for essential updates.

🔍 In the wild

Slack broadened its notification system beyond direct messages and mentions. Duolingo built a high-scale notification system for its engagement campaigns.

App size#

Size affects acquisition and retention — Google's research shows each extra 6MB of APK cuts install conversion by roughly 1%, worse on limited data plans and constrained storage. The two big contributors are third-party libraries (each should justify its footprint) and media assets (use vector graphics, on-demand downloads, server-side image optimization). Name platform tools: R8 code shrinking, resource shrinking, and App Bundles on Android; app thinning, on-demand resources, and whole-module optimization on iOS. Instagram Lite launched at just 573 KB.

CI/CD#

Continuous Integration builds, tests, and statically analyzes on every push — via Fastlane, GitHub Actions, or Bitrise — turning problems into immediate feedback. Continuous Delivery automates signing, environment config, release builds, and distribution to test environments; mobile signing is distinctive (Android keystores; iOS provisioning profiles and certificates, often via Fastlane Match), and each environment needs its own config (Gradle flavors/buildConfigField; Xcode configurations/schemes). True Continuous Deployment is capped by app-store review, so releases aren't instant. The payoff is consistency — the same proven process every time.

🔍 In the wild

Duolingo cut Android and iOS build times 68%. Pinterest's iOS team moved to Bazel and cut clean-build time 21%.

Security#

Security protects user data, system integrity, and access while keeping the experience smooth. Rather than deep implementation, know the domains:

Device security Jailbreak/root detection, app-integrity checks (Play Integrity, App Attest), secure local storage, code obfuscation (R8).
Network security TLS everywhere, certificate pinning against MITM, API hardening, secure offline operation.
Data security Encryption at rest, secure key management, minimal PII handling, data minimization.
Security vs. UX Stronger checks for high-risk actions, clear permission rationale, protection proportional to sensitivity.

Client-side security is one layer — it only works alongside a well-secured backend. Signal centers its whole design on privacy, with end-to-end encryption that keeps even its own servers from reading user content.

12

One app, many screens and OS versions#

Fragmentation across hardware, screen sizes, and OS versions shapes design choices. These topics may not lead an interview but often surface in the deep dive, where showing you've considered them signals maturity.

Form factors#

The strategy is strategic modularization: business logic and the data layer stay consistent across devices; the UI and navigation layers adapt. Expectations shift by device — tablets show list and detail side by side; foldables transform between phone and tablet states dynamically; wearables demand glanceable, simplified interfaces; TVs need remote-friendly layouts viewed from a distance. Declarative UI (Jetpack Compose, SwiftUI) makes adaptive layouts far easier. Some data-layer tuning helps too — caching by available memory, per-form-factor image resolutions, prefetching tuned to expected connectivity.

🔍 In the wild

Spotify adapts across screen sizes for a consistent experience. Google Photos added responsive layouts for tablets, foldables, and ChromeOS. Microsoft optimized Outlook, Teams, and Office for large-screen Android with multi-window and multi-instance support, lifting active users and retention.

OS and SDK versions#

Version support trades reach against complexity. The minimum version sets which devices can run the app — a data-driven call weighing OS distribution, required platform capabilities, testing capacity, and the security profile of old versions. The target version (usually the latest stable release) determines which new features you can use; update it regularly for performance, security, and modern APIs. The gap between them dictates how much version-specific code you carry. Manage API deprecation proactively — review warnings, plan migrations, test on old and new. Test strategically: full coverage on min/target/one middle version, smoke tests everywhere, deeper tests on version-specific code.

📌 Adoption differs by platform

iOS users upgrade faster; Android users are often held back by manufacturers or device age. Reassess minimum-version requirements as analytics shift, using each major release as a chance to drop a diminishing tail of old versions.

13

Recap#

Principles over frameworks Separation of concerns, one-way data flow, immutability, SSOT, and DI carry any answer.
Two layers plus a repository UI and data (optional domain); repositories hide data sources behind interfaces.
Modules buy build speed Contract modules keep dependents from rebuilding on impl changes.
Storage by shape Security → relationships → structure → shape picks the mechanism; real apps mix them.
REST + JSON as default Consistent verbs, plural nouns, global versioning, standard status codes.
Stateless tokens Short access token on every request, refresh token to renew, both in secure storage.
CDN for heavy statics Serve bandwidth-heavy, stable content from the edge; origin as fallback.
Cursor scales Offset for stable sets with random access; cursor for large, churning feeds.
Measure, then optimize Baseline, monitor in production, validate each change incrementally.
Ship gradually Staged rollouts, flags, and severity thresholds decouple release from risk.
🎯 The single idea underneath it all

Every building block here is a way of drawing a boundary — between layers, between data owners, between availability and deployment, between what's cached and what's authoritative. Mobile system design is mostly the discipline of putting those boundaries in the right places and defending the trade-offs each one makes.

14

Further reading#

Grouped by topic, each with a one-line reason to read it. Every link was fetched and confirmed live on 24 August 2026. Six source links did not resolve and are omitted — see the note at the end.

Architecture & patterns#

[1]
The Clean Architecture The dependency rule and layered separation that underpin most of this chapter.
[2]
Google's guide to app architecture The two-layer (plus domain) reference model used throughout.
[3]
Unidirectional Data Flow Why one-way flow makes state predictable.
[4]
The repository pattern How repositories abstract data sources.
[11]
Separation of concerns The base principle for keeping UI logic isolated.
[12]
Reactive programming The stream-based model used to implement UDF.
[13]
Single Source of Truth Why each data type needs one owner.
[14]
Dependency Injection Supplying dependencies instead of constructing them.
[15]
Service Locator pattern The alternative this book deliberately avoids.
[19]
UI layer State holders and how UI state is produced.
[20]
Data layer Repository responsibilities and data sources.
[21]
Domain layer When the optional middle layer earns its place.
[22]
Mobile deep linking The extra navigation entry points real apps must handle.
[23]
Guide to app modularization Expands the contract-module pattern.

UI-layer architectures#

[5]
MVC Apple's classic presentation pattern.
[6]
MVVM The view-model pattern UDF often maps onto.
[7]
VIPER A more granular iOS architecture.
[8]
Uber's RIBs Cross-platform architecture for deeply nested state.
[9]
Airbnb's Mavericks Android MVI framework.
[10]
Slack's Circuit Compose-first UDF architecture library.

Architecture in practice#

[16]
Duolingo's architecture improvements MVVM plus Jetpack and Hilt for DI.
[18]
Notion going native Flexible data model and unidirectional flow on mobile — also the source for weekly releases.
[24]
Airbnb iOS modularization Restructuring into ~1,500 modules for productivity.
[25]
Robinhood's module monolith breakup Splitting a giant module to speed builds.

Testing#

[26]
Fundamentals of testing Android apps Google's baseline testing guidance.
[27]
The robot pattern Keeping UI tests readable and maintainable.
[28]
OHHTTPStubs Faking backend responses on iOS.
[29]
MockWebServer Faking HTTP on Android.
[30]
Slack's Android UI automation Making hermetic UI tests first-class.
[31]
Bitrise's mobile testing pyramid A recommended balance across test types.
[32]
Netflix's testing at scale Device coverage as a two-dimensional matrix.
[33]
Dropbox's testing strategy Testing to keep trust at scale.

Data storage#

[34]
SharedPreferences Android key-value basics.
[35]
DataStore Modern typed key-value on Android.
[36]
UserDefaults iOS key-value store.
[37]
Property Lists iOS Plist storage.
[38]
Android Keystore Hardware-backed key storage (also [64]).
[39]
iOS Keychain Services Secure credential storage (also [65]).
[40]
Jetpack Room Android relational persistence over SQLite.
[41]
Core Data Apple's relational persistence framework.
[42]
Realm Non-relational mobile DB with reactive sync.
[43]
Firebase Realtime Database Cloud NoSQL with offline sync.
[44]
iOS FileManager Direct file I/O on iOS.
[45]
Android data & file storage Overview of Android's storage APIs.
[46]
Netflix downloads on Android Choosing Realm for offline video metadata.
[47]
Trello's offline-first architecture Making the local DB the single source of truth.
[48]
iOS file directories The available system directories.
[49]
PhotoKit — fetching assets Reading the photo library on iOS.
[50]
UIDocumentPickerViewController Letting users pick files anywhere on device.
[51]
Android File APIs Storage Access Framework and shared documents.
[52]
Content Providers Sharing data between your own apps (permissions at [53]).
[54]
iOS App Groups Shared containers for same-team apps.

Networking & API design#

[56]
HTTP/1.1 semantics (RFC 7231) The methods and status codes REST builds on.
[57]
What is REST A gentle intro to REST conventions.
[58]
CRUD operations The verbs REST maps onto.
[59]
Lyft's mobile networking journey Protocol Buffers, gRPC, and content negotiation.
[60]
Reddit's move to GraphQL Serving mobile clients from a GraphQL layer.
[61]
Airbnb with GraphQL and Apollo Scaling development with GraphQL.
[62]
Snapchat's QUIC adoption Faster mobile networking with QUIC.

Authentication#

[63]
JSON Web Tokens The compact, self-contained token format.
[67]
Robinhood's trusted devices Balancing MFA with convenience on known devices.

Pagination#

[68]
Cursor pagination at Meta Graph API's cursor-based paging in practice.
[69]
Cursor pagination at Slack Slack's API paging conventions.
[70]
Slack's migration to cursor paging Why they moved off offset pagination.
[71]
Discord's list virtualization Cutting startup memory for infinite lists.
[72]
Shopify's relative cursors Benchmarks showing offset degrade and cursor win.

Performance#

[73]
Play Store downranks slow apps Quality signals affecting search and listings.
[74]
Why users uninstall apps Crashes, data use, and jank drive churn.
[77]
Robinhood's Server-Driven UI Shipping UI changes without app releases.
[78]
Spotify's client modernization Batched metadata and precomputed sorting.
[79]
Lyft's ANR reduction Disk-read optimization and memory caching.

Releasing & experimenting#

[81]
Google Play staged rollouts Percentage rollouts with halt and resume.
[82]
Apple's phased releases The fixed 7-day release schedule.
[83]
TestFlight Distributing builds to external testers.
[84]
Building apps at scale (Bitrise Q&A) Includes Uber's approach to force-upgrading.
[85]
Netflix's experimentation platform The "ABlaze" A/B testing infrastructure.
[86]
Duolingo's A/B testing Building experimentation into the app.
[87]
Pinterest's JSON logging system Schemaless client-side logging.
[88]
Slack's notification tracing Modeling notification flows as traces.
[89]
Signal's stance on analytics Collecting virtually none, by design.

Localization#

[90]
Exporting localizations (iOS) Built-in translation export.
[91]
Android localization Localizing through the resource system.
[92]
Netflix's global interface Ingesting, translating, and serving UI strings.
[93]
Netflix's pseudo-localization Testing layouts survive other scripts.

Privacy & security#

[94]
GDPR The EU data-protection framework.
[95]
UK Data Protection Act UK data-protection standards.
[97]
Australian Privacy Principles Privacy rules for Australian apps.
[98]
OWASP Mobile Top 10 Annually updated mobile risk list — note the 2023 refresh.
[99]
Meta's Federated Learning with DP Improving products without exposing data.
[118]
iOS jailbreaking The compromise state detection guards against.
[119]
Android rooting The Android equivalent to detect.
[120]
Play Integrity API Verifying app authenticity on Android.
[121]
iOS App Attest Ensuring the app runs unaltered.
[122]
R8 code shrinking Obfuscation plus shrinking on Android.
[123]
Certificate / public-key pinning Background on pinning against MITM (note: HPKP itself is deprecated in browsers).
[124]
Personally Identifiable Information What "minimize PII" refers to.
[125]
Signal's privacy approach End-to-end encryption as the design center.

Accessibility#

[100]
WCAG 2.1 The definitive accessibility guidelines.
[101]
Apple Human Interface Guidelines iOS design and accessibility guidance.
[102]
Material Design accessibility Android's inclusive-design principles.
[103]
BBC Mobile Accessibility Guidelines A strong, technology-agnostic public reference.

Push notifications#

[104]
Apple Push Notification service The iOS delivery intermediary.
[105]
Firebase Cloud Messaging The Android delivery intermediary.
[106]
Slack's notification system Broadening notification types at scale.
[107]
Duolingo's notification system High-scale delivery for engagement campaigns.

App size & CI/CD#

[108]
Shrinking APKs, growing installs The ~1%-per-6MB conversion finding.
[109]
Reduce Android app size R8, resource shrinking, App Bundles.
[110]
Reduce iOS app size App thinning and on-demand resources.
[111]
Instagram Lite A 573 KB build for emerging markets.
[112]
Fastlane Automating build and release steps.
[113]
GitHub Actions CI/CD from the repository.
[114]
Bitrise Mobile-focused CI/CD.
[115]
Fastlane Match Managing iOS signing certificates.
[116]
Duolingo's CI/CD speedups Cutting build times 68%.
[117]
Pinterest's Bazel iOS builds A 21% clean-build improvement.

Supporting different devices#

[126]
Android large screens Designing for tablets and foldables.
[127]
Apple layout guidance Adapting layouts across form factors.
[128]
Spotify across screens A developer story on multi-form-factor support.
[129]
Google Photos on large screens Responsive layouts for tablets and ChromeOS.
[130]
Microsoft on large-screen Android Multi-window and multi-instance lifting retention.
🔗 Source links omitted (did not resolve on 24 Aug 2026)

Six of the source's references failed verification and were left out rather than linked to broken pages: Uber's plugin-system/RIBs post [17] and Uber's passkeys post [66] (both return 406 to automated fetches — reachable in a signed-in browser); Instagram Engineering's prefetching [75] and Data Saver [76] posts (repeated timeouts); Uber's failover-handling post [80] and the CDC HIPAA page [96] (both return 404 — genuinely moved or removed). Reference [55], a malformed init(suiteName:) link, is broken in the source itself. Their concepts are all covered in the prose above.

Rewritten from scratch based on ByteByteGo's Mobile System Design Interview — "Mobile System Design Building Blocks." No source phrasing was copied.

All diagrams are original. [+] marks context added beyond the source. Every further-reading link was fetched and confirmed live on 24 August 2026; six unresolved source links are listed above rather than included.