Irbisa · Cheatsheet

Mobile System Design

Offline-first, sync, pagination, caching, modularization, feature toggles

Senior Developer · 20 questions

  1. 01

    In a mobile system design round, how do you structure the 45 minutes so you cover what the interviewer is grading?

    Clarify scope first, then define the API and data model, sketch the client architecture, go deep on the two or three hardest parts of this product, and close with cross-cutting concerns and trade-offs.

  2. 02

    You like a post on the detail screen, go back, and the feed still shows it unliked. What is wrong with the architecture?

    Two screens hold two copies of the same post, so the like updates one copy and the other never hears about it.

    // Wrong: each screen owns a copy, so a write on one is invisible to the otherclass DetailViewModel(private val api: Api) : ViewModel() {  val post = MutableStateFlow<Post?>(null)   fun like(id: String) = viewModelScope.launch {    post.update { it?.copy(liked = true) }   // only this screen will ever know…
  3. 03

    How do you design search-as-you-type so it stays responsive without hammering the backend?

    Debounce the input, cancel the previous request when a new query arrives, and show local results while the network catches up.

    private val query = MutableStateFlow("") val results: StateFlow<SearchUi> = query  .map { it.trim() }  .distinctUntilChanged()  .debounce(250)…
  4. 04

    Product wants every screen fresh, and the app still fires forty requests on cold start. How do you decide what to cache and for how long?

    Freshness is decided per resource, not with one app-wide number: sort each endpoint by how bad it is to show it stale, and give each class its own cache policy.

    // One policy per resource class, not one number for the whole appenum class Freshness(val ttl: Duration) {  NEAR_STATIC(24.hours),   // serve from cache within the ttl  USER_OWNED(5.minutes),   // serve from cache within the ttl  VOLATILE(Duration.ZERO)  // never render silently from cache}…
  5. 05

    Opening the profile screen fires seven requests and it stays blank until the slowest one returns. What do you change, on both sides?

    First paint must not wait for the slowest of seven calls: collapse the round trips into one screen-shaped endpoint, and render each section as soon as its own data arrives.

    // Wrong: awaitAll means the screen is blank until the slowest call finishesval state = coroutineScope {  val header = async { api.header(id) }  val stats = async { api.stats(id) }  val recs = async { api.recommendations(id) }   // 900 ms p95, blocks the name  ProfileUi(header.await(), stats.await(), recs.await())…
  6. 06

    iOS shows one order total, Android shows another, and the emailed receipt shows a third. What went wrong in the design?

    Three clients are each computing a number that only one system may own: the total must be calculated once, on the server, and only displayed everywhere else.

  7. 07

    A limited offer must end at midnight, but some users see it for another day. Which clock can the app trust?

    Not the device clock: the wall clock can be changed by the user, drifts and jumps, so anything with a deadline or an ordering must be anchored to server time.

    // Wrong: the countdown and the gate both trust a clock the user can changeval remaining = offer.endsAtMillis - System.currentTimeMillis() // Right: one anchor, refreshed on every response, used wherever "now" is neededclass ServerTime {  private data class Anchor(val serverMs: Long, val elapsedMs: Long)…
  8. 08

    Six months after a release, people are still opening that old build. What does that force you to design differently?

    You cannot retire a shipped mobile build, so every API change must stay compatible with clients you no longer control, and the important levers must live on the server rather than in the binary.

  9. 09

    How would you design the feed screen of an Instagram-like app so scrolling feels continuous and survives rotation and process death?

    Read the feed only from the local database, request the next page a few rows before the end with one request in flight, de-duplicate by id, and persist the pages and the cursor, so scrolling never stops and rotation or process death costs nothing.

    class FeedViewModel(private val repo: FeedRepository) : ViewModel() {  val items: StateFlow<List<FeedItem>> = repo.observeFeed()      // rows from the local DB    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())   private var nextCursor: String? = null                         // persist it in a real app  private var endReached = false…
  10. 10

    How do you run feature flags and remote config in a large app without ending up with hundreds of stale branches?

    A feature flag is a switch that turns code on or off without a release, and the danger is that flags pile up: every flag needs an owner, an expiry date and a planned deletion.

    // One registry: every flag has an owner and a removal date, so deletion is scheduled workenum class Flag(val key: String, val owner: String, val removeBy: LocalDate?) {  CHECKOUT_V2("checkout_v2", owner = "payments", removeBy = LocalDate.parse("2026-12-01")),  NEW_SEARCH("new_search", owner = "discovery", removeBy = LocalDate.parse("2027-01-15")),  PAYMENTS_ENABLED("payments_enabled", owner = "payments", removeBy = null), // permanent kill switch}…
  11. 11

    Design an in-app cache with memory and disk layers: how do you implement LRU eviction, size limits and thread safety?

    Two tiers behind one interface: a small in-memory LRU bounded by bytes, not by entry count, and a larger disk LRU; reads go memory → disk → network and promote on a hit.

    class LruMemoryCache<K : Any, V : Any>(  private val maxBytes: Long,  private val sizeOf: (V) -> Long,) {  // accessOrder = true: every get() moves the entry to the tail (most recently used)  private val map = LinkedHashMap<K, V>(16, 0.75f, true)…
  12. 12

    How would you design an offline-first app: local source of truth, sync on an unreliable network, and conflict handling?

    The UI reads only the local database and the network only refreshes it.

    class NotesRepository(  private val db: AppDatabase,  private val work: WorkManager,) {  fun observe(): Flow<List<Note>> = db.notes().observeAll()      // the UI reads only this…
  13. 13

    How would you design a chat app like WhatsApp: transport, delivery states, the offline queue and message history?

    The local database is the source of truth for the screen, a WebSocket delivers live events, REST pages the history, and every outgoing message gets a client id so it shows at once and is matched with the server copy when the ack arrives.

    suspend fun send(chatId: String, text: String) {  val clientId = UUID.randomUUID().toString()  db.withTransaction {    messages.insert(      MessageEntity(clientId = clientId, chatId = chatId, text = text,        serverSeq = null, state = State.SENDING)            // shown at once, at the bottom…
  14. 14

    How would you design an image-loading library like Coil or Glide?

    A request pipeline: key → memory cache → disk cache → network, then decode at the target size off the main thread, all tied to the view's lifecycle so a recycled row cancels its old request.

    class ImageLoader(  private val memory: LruCache<String, Bitmap>,          // sizeOf() returns byteCount  private val disk: DiskCache,  private val fetcher: Fetcher,  private val decodeDispatcher: CoroutineDispatcher = Dispatchers.Default.limitedParallelism(2),) {…
  15. 15

    Design a vertical short-video feed like TikTok or Reels: prefetching, a pool of players, caching segments, and starting playback instantly on swipe.

    A VerticalPager over a paged list of clips, a pool of three ExoPlayers bound to the current clip and its neighbours, and a preload manager that keeps the first seconds of nearby clips in memory and on disk, so a swipe lands on a clip whose data and first frame are already there.

    @OptIn(UnstableApi::class)class ShortsPlayback(context: Context, cache: Cache, clips: List<Clip>) {  private var current = 0  private val builder = DefaultPreloadManager.Builder(context) { index: Int ->    when (abs(index - current)) {      1 -> PreloadStatus.specifiedRangeLoaded(2_000L)            // neighbours: 2 s in memory…
  16. 16

    Design a download and upload manager for large files: queueing, retries, resume and progress that survive the app being killed.

    Persist every transfer as a row in a queue, run it as durable background work the OS resumes after process death, move bytes in resumable chunks, and expose progress from that row.

    class DownloadWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {  override suspend fun doWork(): Result {    val job = dao.get(inputData.getString("id")!!) ?: return Result.failure()    val part = File(job.targetPath + ".part")    val done = if (part.exists()) part.length() else 0L…
  17. 17

    Design an analytics and logging SDK for the app: batching, an offline buffer, sampling and swapping vendors.

    A typed track() that only appends to a local queue; a separate uploader batches by count or time, survives process death and fans out to vendor adapters behind your own interface.

    interface AnalyticsSink { suspend fun send(batch: List<StoredEvent>) }  // vendor adapters class Analytics(  private val queue: EventDao,  private val scope: CoroutineScope,  private val sampler: Sampler,…
  18. 18

    When is server-driven UI worth it, and what goes wrong when a team takes it too far?

    Server-driven UI means the backend sends a description of a screen and the client renders it from a fixed set of components it already ships.

    @Serializablesealed interface Block {  @Serializable @SerialName("hero")  data class Hero(val imageUrl: String, val title: String, val action: String) : Block   @Serializable @SerialName("product_row")…
  19. 19

    How would you design the client of a ride-hailing or nearby-friends app that shows live locations without draining the battery?

    Split it into publishing your own location and watching other people's: publish through the fused provider with accuracy that follows the trip state, send only meaningful moves over one connection, and animate others' markers between sparse updates instead of asking for more.

  20. 20

    How would you structure a super-app where a dozen teams ship features into one Android app?

    Make each feature a pair of modules that talks to others only through small api modules and a few shared core modules, with the :app shell wiring everything through DI and navigation.