Mobile Design Rounds
Case studyAndroidiOS20 min read

Design reels, video and photos for a travel app (Android and iOS)

Last reviewed . Drafted with AI assistance. Checked against official documentation where noted (how this is checked). Part of: The five-step method · Staff-level guide.

Build it. The tables, REST calls, packaging asks, Kotlin and Swift code and platform limits are on a separate page: Build it: reels, video and photos on Android and iOS.

How to read this. This page is the 45 minutes, in the order I would spend them. It uses a travel app as the example. The same design fits any app with short videos and big photos. It builds on three other cases: the video streaming app (long videos, downloads), the image loading library and photo backup. The follow-up questions are my reconstruction from public prep material, not transcripts of real rounds.

By the end you can:

  • Say why one player per reel cell fails, and what owns the players instead.
  • Say how much of the next reels you load, and why not more.
  • Explain why a 12 megapixel photo costs 48 MB in memory, and what you decode instead.
  • Explain what happens when the app is killed in the middle of an upload, and why the upload ID is saved first.

Read sections 4.2 and 4.3 twice. Most answers miss them.

TL;DR. My answer: on a phone the limits are decoders, memory and the data plan, not the CDN. I keep a small pool of two or three players that lives outside the screens. I load only the first seconds of the next reels, and I show a poster until the first frame is on screen. I decode photos at the size the screen needs. I never decode the original. For posting, I compress on the phone, save the file and the IDs before any network call, and upload in chunks that resume from the server offset, with the same upload ID after a restart. One media budget owns the memory and disk limits, and it shrinks when the system warns about memory.

Patterns in this round.

  • Scoped owner (the player pool and the uploads live as long as the app needs them, not as long as a screen)
  • State machine (a post goes through a short list of states)
  • Outbox (a post waits on the phone until the server has it)
  • Idempotency key (the upload ID and the post ID make a repeat safe)
  • Layered cache (memory, then disk, then the CDN)

More in design patterns in mobile rounds.

Free: one follow-up round a week. A short interview question with a weak and a strong answer, by email. You also get the cheat sheet.

1. Scope and constraints (minutes 0 to 4)

In short: Short vertical videos in a feed, big photos on a place page, and posting your own trip videos and photos. Long videos, live and editing effects wait.

A prompt I would expect: "Design the media part of a travel app. People watch short trip videos, look at photos of a place, and post their own." I ask five questions, with a default for each:

  • Short videos only, or long ones too? Short, up to 60 seconds, vertical. Long videos are the video app case.
  • Do people post? Yes, from the camera or from the gallery. One video or up to ten photos in a post.
  • How big are the photos? Up to 48 megapixels, with zoom on the place page.
  • Offline? The feed needs a network. A post made offline must upload later and must not be lost.
  • Sound? Muted at first. The user turns it on once, and the app remembers.
In scopeOut of scopeParked until you ask
Reels feed with autoplay and swipeRanking and recommendationsLive video
Place gallery and a photo viewer with zoomComments and likesFilters, music and editing
Post a video or photos from the phoneModeration toolsDownloads for offline watching
Cache and memory limitsAdsBackup of the whole gallery

Four things the phone does to this design:

What the phone doesRequirement at riskDeep dive
Allows only a few video decoders at once, and little memorySmooth swipe with no crashPlayer pool (3), photos (4.3)
Has a data plan and a batteryData spent on purpose, no wasted preloadPreload (4.1)
Kills the app, or loses the network, in the middle of an uploadA post is never lost and never doubledUpload (4.2)
Clears cache folders when storage is lowA pending upload must not live in a cache folderUpload (4.2)

Your decision. The interviewer says: "Add filters, music and a video editor." Do you say yes, or keep it parked?

First idea, and why it fails. Say yes and design the editor. Then there is no time for the two parts that decide whether the app works: smooth playback and uploads that survive.

What I would say. I keep playback, photos and posting. An editor changes the CPU and the file size before upload, but not the shape of the pipeline. I design the pipeline so an editing step can sit in front of it.

Say this. "Short vertical videos, big photos, and posting from the phone. Ranking, live and editing are out for now. I will leave a place in the upload path for an editing step. Tell me if you want it earlier."

The follow-up question I expect: "Why not just use the long video design?"

Three things differ. A feed shows several videos at nearly the same time, so decoders and memory become the limit. People start and stop videos every few seconds, so time to first frame matters more than the best bitrate. And users upload their own clips, so the phone has to compress and send large files.

2. Data and contracts (minutes 4 to 14)

In short: A feed row is small and has a poster. The playback address comes from a separate call. Every upload and every post has an ID made on the phone and saved before any network call.

Your decision. What does a reel row carry, and how do the video bytes reach the player?

First idea, and why it fails. One MP4 address in every feed row, and the player streams it. But a signed address expires. One 1080p file is too big for a weak link. And with no poster the user sees a black rectangle until the first frame.

What I would say. The feed row carries the id, a poster address, a tiny blurred preview or dominant colour, the duration and the aspect ratio. A playback call, made when a reel is about to be preloaded, returns the video address and its expiry. Packaging gives me a few renditions of each reel. I reject putting the video address in the row, and I reject one big file.

The video format is a packaging choice. I say what I need and what each option costs:

OptionGoodCosts
HLS with short segments (about 2 s)The player can change quality. Preload by segment works the same on both platformsMore requests before the first frame: playlist, then segment
One MP4 per quality, with the index at the front of the fileOne request to the first frame. Easy to cache by byte rangeNo quality change inside a video. On iOS, the disk cache needs extra code

My default is HLS with 2 second segments and a low first rung, because reels are short but networks change. If packaging gives only MP4, I pick the quality once per reel from a recent bandwidth estimate. The reel is short, so I live with it.

The follow-up question I expect: "The playback address expires while the user watches."

I call playback again and use the new address. The cache key must not contain the signed part of the address, or the cache misses. I build the key from the reel ID and the rendition. With that key, bytes already on disk are still found after a new address.

I decide if a signed address is still valid from the expiry time the server sent, plus a safety margin. I call playback again on any 403. I do not trust the phone clock, because the user can change it.

Another follow-up: "The server removes a reel the user has in the feed."

The playback call answers 404 or 410. The app removes the row, with a tombstone in the local database so a stale feed page cannot bring it back. The pager moves on with no error message.

Another follow-up: "The feed refreshes while the user is on reel 4."

I do not reorder the list under the user. New reels are added after the current position, and the pager keeps the current reel ID as its anchor. A refresh that changes the order changes only what comes next.

The upload contract is where most answers go wrong. These are my design choices:

CallWhat it carriesWhy
Create upload sessionuploadId (a UUID made on the phone), size, typeThe same ID always returns the same session. A repeat after a timeout is safe
Ask for the offsetuploadIdThe server says how many bytes it holds. The app continues from there
Send a chunkoffset, bytesThe server ignores bytes it already has
Create the postpostId (a UUID made on the phone), upload IDs, caption, placeThe same post ID gives the same post. A double tap gives one post
Read the postpostIdState: processing, ready or rejected. A push says when it changes

Your decision. When does the phone make the upload ID and the post ID?

First idea, and why it fails. When the request is sent. Then a retry after a timeout makes a new ID, and the server sees a second post. After a restart the app has forgotten the ID, so it starts a second copy.

What I would say. When the user taps Post, before any network call. I write both IDs, the file path and the caption to the local database in one transaction. Every retry and every restart uses the saved IDs. I reject making the ID in the request code.

Say this. "The IDs exist before the first request. They are on disk. After a kill or a timeout, the app asks the server about the same IDs. It never makes new ones for a post that already exists."

The idempotency key guide has the general rule. Resumable sessions exist in the big storage services. Google Cloud Storage needs chunks in multiples of 256 KiB and keeps a session for one week (resumable uploads). Amazon S3 multipart uploads need parts of at least 5 MiB, except the last, and allow up to 10,000 parts (S3 limits). Whatever the backend, the phone needs two things from it: an offset it can ask for, and a session that accepts the same ID twice.

3. Client structure (minutes 14 to 22)

In short: A small pool of players, owned outside the screens. One media budget for memory and disk. An upload manager that outlives the composer screen.

Your decision. Who owns the players: each reel cell, the feed screen, or an app-level pool?

First idea, and why it fails. Each cell creates its own player when it appears. This is the easy code. But a fast swipe makes ten players in a second. Each player holds a decoder (the part that turns video data into pictures), and a phone has only a few hardware decoders. When they run out, a new player fails, or falls back to a slow software decoder, and the user sees a black screen.

What I would say. A pool of two or three players, owned by an app-scoped object. The feed screen leases a player for the current page and one for the next page, then gives them back. A cell owns only the surface it draws on and its poster image. I reject one player per cell, and I reject a single player, because the next reel could not have its first frame ready.

Say this. "Players are a scarce resource, so something with a bigger lifetime than a cell owns them. The pool has two players on a low-memory phone and three on a normal one. A cell never creates a player."

Android reports a limit per codec through getMaxSupportedInstances() on the codec capabilities. I did not read its page for this case, and the real number depends on the phone, so I test on cheap devices.

The follow-up question I expect: "Why not one player that switches the video?"

With one player the next reel cannot load while the current one plays. Every swipe starts from zero: new source, new buffer, new first frame. With a second player, the next reel is ready and paused on its first frame when the swipe ends. A third player keeps the previous reel for a swipe back. The cost is one more decoder and its buffers, so the pool size follows the device class.

Another follow-up: "A call comes in, or the user unplugs the headphones."

Pause every player, show the paused state, and do not start again by itself. On Android, the audio focus change and the "becoming noisy" event are the signals. On iOS, the audio session interruption and the route change notifications are the signals. A call is a normal event in a feed, not an error.

eventsleasespostcallschunkspost rowswarmsbytescapsUIreels pager, gallery, composerState holdersViewModels, uiStatePlayer pool2 or 3 players, app scopedPreload managerfirst seconds of next reelsCDNvideo and photo variantsUpload manageroutbox, saved IDsUpload storageresumable sessionsMedia repositorylocal DB is the SSOTREST APIfeed, playback, postsMedia budgetmemory and disk capsImage pipelinesizes, decode, memory cacheUIreels pager, gallery, composerState holdersViewModels, uiStatePlayer pool2 or 3 players, app scopedPreload managerfirst seconds of next reelsCDNvideo and photo variantsUpload manageroutbox, saved IDsUpload storageresumable sessionsMedia repositorylocal DB is the SSOTREST APIfeed, playback, postsMedia budgetmemory and disk capsImage pipelinesizes, decode, memory cache

More links

  • UI to State holders: events
  • State holders to Player pool: leases
  • State holders to Upload manager: post
  • Media repository to REST API: calls
  • Upload manager to Upload storage: chunks
  • Upload manager to Media repository: post rows
  • Preload manager to Player pool: warms
  • CDN to Preload manager: bytes
  • Media budget to Image pipeline: caps
Players and uploads live outside the screens. One budget caps memory and disk for images, video and pending uploads.

Four rules:

  • The screen never creates a player and never touches the network.
  • Only the page that has settled plays. While the finger moves, nothing starts.
  • Memory and disk limits live in one place, the media budget, not in five libraries with five defaults.
  • Pending uploads live in the app files folder. They are never in a cache folder, because the system may clear a cache folder when storage is low.

The feed is a vertical pager. On Android that is a vertical pager in Compose or ViewPager2 with vertical orientation. On iOS it is a paging collection view or a paging scroll view. Neither platform changes the design.

4. Make the hard part work (minutes 22 to 40)

In short: Load a few seconds, not whole videos. Save before you send. Decode photos at the size on screen.

4.1 Play and preload without waste (minutes 22 to 28)

Your decision. The user swipes to the next reel. How much of the next reels do you load before they ask for it?

First idea, and why it fails. Load the next five reels completely. At 720p a 30 second reel is about 9 MB, so that is about 47 MB, and most of it is wasted when the user stops on reel two. It also competes with the reel on screen for bandwidth, and every prepared player holds a decoder.

What I would say. A window around the current reel, as in the diagram. The next reel gets a ready player and about 5 seconds of data. The one after it gets 1 to 2 seconds of bytes on disk and no player. Further reels get only the poster. On cellular or with Data Saver on, only the next reel, and only 2 seconds. I show the poster until the player says the first frame is drawn. I reject whole-video preload.

Previous reelkept briefly, paused on a frame-1Current reelplays and loops, muted at first0Next reelplayer ready, about 5 s buffered+1Two aheadfirst 1 to 2 s of bytes on disk+2Further aheadposter image only+3
The preload window. A few seconds of the next reels, never whole videos.

The numbers, as my assumptions: 720p at 2.5 Mbit/s is about 310 KB/s. Five seconds is about 1.6 MB, and two seconds is about 0.6 MB. One settled swipe costs about 2.2 MB of preload, not 47 MB.

Android has this built in. DefaultPreloadManager in Media3 takes the index of the current item and asks my code how far to preload each item, and its example uses 5 seconds for the next item and 3 seconds for the previous one (Media3 preloading, part 1). On iOS, a player item buffers on its own once it is assigned to a player, and preferredForwardBufferDuration sets how many seconds it should buffer. Apple says a low value raises the chance of a stall, and a high value uses more system resources (Apple).

Say this. "I measure time to first frame, the share of plays that stall, and the preload bytes that were never played. If the last number is high, my window is too deep."

The follow-up question I expect: "The user swipes ten reels in three seconds."

Nothing starts while the pager moves. When it settles on a reel, I wait a short time, about 150 ms (my number), then prepare that reel and the next. Any preload for a reel the user skipped is cancelled. The skipped reels cost nothing but their posters.

Another follow-up: "The network drops while a reel plays."

The player keeps playing what it has buffered, then shows the poster and a small spinner. After a few failed retries it shows "Could not load. Tap to retry". It does not skip to the next reel by itself, because the user might then miss the one they wanted.

Another follow-up: "Sound."

Reels start muted. When the user turns sound on, the app remembers it, and the next reels start with sound. A muted reel must not take audio focus on Android, and on iOS I pick the audio session category by intent: a muted video should not stop the user's music. This part is my reading and I did not verify it against the audio documentation.

4.2 Posting a video that survives (minutes 28 to 34)

Your decision. Someone films 40 seconds in 4K in a hotel with weak Wi-Fi and taps Post. What do you send, from where, and what runs it?

First idea, and why it fails. Send the camera file as it is, in one request, from the composer screen. A 4K phone clip can be around 45 Mbit/s, so 40 seconds is about 225 MB (an assumption). One dropped connection restarts from byte 0. If the user leaves the screen or the system kills the app, the request is gone. The camera file may also be in a cache folder that the system clears. And two taps on Post send two posts.

What I would say. Seven steps. (1) Copy the picked file into the app files folder at once, because a picker address may stop working. (2) Compress to 1080p at about 8 to 10 Mbit/s on the phone. (3) Write the post row, the upload row and both IDs in one transaction. (4) Show the post in the profile as Posting, playing the local file. (5) Upload in chunks from the saved offset. (6) Create the post with the saved post ID. (7) Wait for ready, and delete the local file only then. I reject the one-request upload.

Saved on the phonefile and IDs written, nothing sent yetnetwork is OKUploadingchunks, offset saved after each oneconnection drops, or a killWaiting or interruptedno network, token trouble, or a killall bytes sentUploaded, processingthe server makes the renditionsserver says readyLivein the feed
One post as a small state machine. Click an event to move it. The post is safe on the phone before anything is uploaded.

Say this. "The file and the IDs are on disk before the first request. After a kill, the app reads the rows and asks the server about the same upload ID. It never makes a new ID for the same file."

What runs the upload:

  • Android 14 and later: a user-initiated data transfer job. The user starts it, it needs a visible notification, and the app must be visible when it is scheduled. The page says no Jetpack library supports these jobs, and that WorkManager with a foreground service is the fallback on Android 13 and lower (user-initiated data transfer). If the user stops the app from the task manager, onStopJob() is not called, so the saved offset must already be on disk.
  • iOS: a background URLSession. The transfer runs in another process and goes on while the app is suspended. Uploads must come from a file, because uploads from data or a stream fail after the app exits (Apple). So I write each chunk to its own file, one task per chunk. iOS 26 adds BGContinuedProcessingTask, which shows progress in a Live Activity and lets the person cancel it (Apple). The video case says a force-quit by the user cancels active background downloads on iOS. I did not check uploads, so I plan for it: the next launch reads the rows and goes on.

The follow-up question I expect: "The token expires in the middle of the upload."

The server answers 401 or 403. The app refreshes the token, asks for the session of the same upload ID, and continues from the confirmed offset. If the refresh fails, the post goes to Waiting and the user sees "Sign in to finish posting". The file stays.

Another follow-up: "The user taps Post twice, or taps Post, kills the app and posts again."

The first tap wrote the rows, so the second tap finds the same row for the same file and does nothing. If the user picks the same video again, the app compares the file size and a quick hash, and offers "This looks like a post you already made". The server also dedupes by post ID.

Another follow-up: "The upload session expires."

The server answers 404 or 410. The app asks for a new session with the same upload ID and starts from byte 0. Google Cloud Storage keeps a session for one week. A phone that waited for Wi-Fi for longer than that pays the full upload again. That is rare and acceptable.

Another follow-up: "The user picks 10 photos from the gallery."

Each photo is a separate upload with its own ID, in one post. I resize to a long edge of about 2560 px, apply the rotation from the metadata when I re-encode (or the photo shows sideways), and remove the location from the file by default. The user picks the place of the post on purpose. A hotel or a home address in the metadata is a privacy risk. This is my choice.

4.3 Large photos and memory (minutes 34 to 38)

Your decision. A place has 400 photos from 12 to 48 megapixels. How do you show a grid and a viewer with zoom without running out of memory?

First idea, and why it fails. Load the file into an image view. But a decoded picture costs width times height times 4 bytes, whatever the file size. The Android guide shows 2048 x 1536 as about 12 MB (Loading large bitmaps). A 12 megapixel photo is 48 MB, and thirty visible cells would be over a gigabyte. On Android that is an out-of-memory crash. On iOS the system can end the app, often with no warning I can act on.

What I would say. Three sizes. The grid gets a thumbnail at the size of its cell in pixels, asked from the CDN. The viewer gets a copy that fits the screen. Only deep zoom uses tiles of the original. I decode straight to the target size: on Android with inSampleSize or ImageDecoder with a target size, on iOS with ImageIO's thumbnail call and a maximum pixel size. I never decode the original and shrink it afterwards. I reject loading the file as it is.

Grid thumbnail360 x 360 px, 4 bytes a pixel0.5 MBViewer copy1080 x 1440 px, fits the screen6 MB12 MP original4000 x 3000 px48 MB48 MP original8000 x 6000 px192 MBOne zoom tile512 x 512 px, a few on screen1 MB each
Memory is width times height times 4 bytes, whatever the file size. These numbers are my arithmetic.

The viewer opens in three steps:

  1. Show the grid thumbnail at once. It is already in the memory cache, so the open animation has no gap.
  2. Load the screen-size copy and fade it in over the thumbnail.
  3. Decode the two neighbours at screen size so a swipe is instant. Release the rest.

The follow-up question I expect: "The system says it is low on memory."

The media budget shrinks in this order, and never touches what is on screen:

  1. Drop the decoded neighbours of the viewer and the decoded images that are off screen.
  2. Empty the image memory cache.
  3. Shrink the player pool to one player and stop all preload.
  4. Keep the disk caches. They cost no memory.

Android calls onTrimMemory with a level, and iOS posts a memory warning. Either system can also end the app for using too much memory with no warning I can act on, so the app has to stay small before any warning comes. I did not verify the list of Android trim levels for this page.

Another follow-up: "How big are the caches?"

CacheWhereMy starting sizeRule
Decoded imagesMemoryA share of the app memory class, counted in bytesCount bytes, not items. 30 big bitmaps are not 30 small ones
Photo filesDisk, cache folder200 to 300 MBLeast recently used goes first
Video bytesDisk, cache folder300 to 500 MBLeast recently used goes first, stable cache keys
Pending uploadsDisk, app files folderAs neededNever evicted. The user can see and cancel them

All sizes in this table are my starting points. Measure on low-end phones and change them with a remote setting.

4.4 Where iOS changes the design

  • Players. Keep the pool at two or three AVPlayer objects. I found no documented limit on how many can run at once, so test on an older iPhone. Show the poster until the player layer reports that it is ready for display.
  • Preload. A player item buffers once it is assigned to a player, and preferredForwardBufferDuration sets how many seconds. I know of no built-in disk cache for it like the Media3 one on Android. My reading is that on iOS the "two ahead" step needs your own small cache, or you skip it. This is my reading, not from a page.
  • Photos. Decode with ImageIO's thumbnail call and a maximum pixel size, so you never hold the original. iOS can end the app for memory with no warning I can act on, so the cost of every decoded image matters before any warning comes.
  • Upload. A background URLSession carries the transfer while the app is suspended. It must upload from a file, so each chunk is its own file. iOS 26 adds BGContinuedProcessingTask for work that the user starts in the foreground, with a Live Activity and a cancel button.
  • Picker. Copy the picked file into the app's own folder at once. Do not keep only the picker's reference.
  • Sound. The audio session category decides whether a muted or playing video stops the user's music, and whether the silent switch is respected. Pick it by intent. I did not verify the category details.

5. Break it, ship it, roll it back (minutes 40 to 45)

In short: Every failure ends in a resumed upload, a poster with a retry, or a smaller cache. Never a lost post and never a second post.

FailureUser seesData holdsRecovery
App killed in the middle of an uploadPosting, then going on after the next startRows, file, upload ID, offsetAsk the server for the offset of the same ID and continue
Fresh upload ID after a restart (a bug)A second copy of the same postTwo uploads for one filePrevented: the ID is saved before any request and read back after a restart
Post tapped twiceOne postOne post row, one post IDSame post ID returns the same post
Token expires mid-uploadA short pauseSession and offsetRefresh the token, same ID, continue from the offset
Upload session expiresPosting again from the startThe local fileNew session for the same ID, from byte 0
The ready push comes before the create answerThe right state anywayThe post IDRead the post by ID. States only move forward: ready never goes back to processing
Disk full while compressing"Not enough space"No partial fileCheck free space first. Delete the partial output on error
No decoder availablePoster and "Tap to retry"NothingReturn the player to the pool, retry once, then show the poster. A software decoder is slow but works
Memory warningPhotos reload a moment laterDisk cachesShrink in the order from 4.3
Reel removed on the serverThe pager moves onA tombstoneRemove the row on 404 or 410
Signed address expiredA short pauseThe cache keyCall playback again. The key does not change
User signs out with a pending uploadUpload cancelledNothing of the old accountCancel the tasks, delete the rows and files. The next user must not see them
Phone is hot or in low power modeA slower compress, no preloadLocal filePause preload. Let the compress finish later

Rows without a source link are my design choices.

At release:

  • Ship the pool size, the preload depth and the upload resolution as remote settings (remote flags), per device class. These are policies, so they can change without a new build. A staged rollout limits the harm of a bad build.
  • The local database layout for pending uploads cannot be flagged off. It ships one release before the code that needs it.
  • The server picks the renditions by what the app supports, so old versions keep working for months.
  • Watch: time to first frame, stall share, playback failures, preload bytes never played, upload success rate, and the rate of crashes from low memory. On Android the exit reason says when the system ended the app for low memory. On iOS, MetricKit reports memory problems. I did not check these two pages.

Say this. "I cannot recall a bad build, so my rollback is the remote setting that lowers the pool and the preload, plus a fixed build. The upload tables ship one release early. I watch time to first frame, stalls, out-of-memory crashes and upload success."

6. Follow-ups that change a constraint

In short: Each follow-up changes one constraint. A strong answer says what breaks.

  1. Crash reports show out-of-memory only on 3 GB phones after a release. Strong: group crashes by device class and screen. Check the pool size and the viewer neighbours first. Lower the pool and preload with a remote setting today, then fix the cause. Weak: "Add more RAM checks."
  2. Creators upload 10 minute 4K videos. Strong: a different path. Cap the length for reels, send long videos on Wi-Fi only, and let the server do the heavy compress. The phone still saves the file and the IDs first. Weak: "Make the chunks bigger."
  3. Many users are on slow 3G. Strong: a lower first rung, 2 seconds of preload for one reel only, and a lower upload bitrate with a clear Waiting state. Weak: "Show a spinner."
  4. Product wants sound on by default. Strong: audio focus and session rules decide it. A video must not stop the user's music without a tap. Keep the user's last choice. Weak: "Just unmute."
  5. The same reel appears twice in the feed. Strong: dedupe by reel ID in the local database, and share one cache entry through the stable cache key. Weak: "The server should fix it."
  6. Add live video (parked). Strong: live needs short segments, no long preload and no offline replay. The server offers it only to apps that support it. Weak: "Same player, new URL."

7. What changes by level

In short: As I read it, levels differ in who raises the problem: the interviewer, the candidate, or the candidate with a plan.

These levels are my own reading. They are not any company's scoring guide.

LevelThe answer coversI want to hear
Mid-levelPlatform player in a pager, a poster, a thumbnail for photos, an upload that retries, a saved ID"I show a poster until the first frame, and I resize photos."
SeniorA player pool outside the cells, a small preload window, settle-based play, decode to the target size, a resumable upload with a saved ID, and a plan for a kill, an expired token and low memory"Players and uploads are scarce and long-lived, so something with a bigger lifetime owns them."
StaffOne media module for the whole app, memory budget by device class, remote policies, metrics for wasted preload and low-memory crashes, and the packaging and storage contract with the backend (staff guide)"Here is how I know the new window is better before I remove the old one."

Common weak answers. One player per cell. Whole-video preload. Loading the original photo into an image view. Uploading in one request from the screen. A fresh ID on every retry. A pending upload in a cache folder. No plan for low memory. They are not data from a study.

Staff answer: the decision and the rollout

  • Decision: one shared media module (player pool, preload policy, image pipeline, upload manager and the media budget) for every app.
  • Alternative: each app team owns its own. Each ships faster at first, but memory limits and upload bugs get fixed several times.
  • Revisit trigger: split the module if one app needs behaviour it cannot get without a fork, or if the module release schedule blocks a team for a cycle.
  • Rollout: the new window and pool behind a remote setting, then a staged release. Compare time to first frame, stall share and low-memory crashes on matched groups (same device class and network).
  • Guardrail: crash-free sessions, playback failure rate and upload success must not get worse than with the old code.
  • Rollback: the old behaviour stays behind the setting. The pending upload tables are migrated one release early, so the setting never needs to roll them back.

What I read and what is my view: I read the facts about Media3 preloading, user-initiated data transfers, background URL sessions, resumable upload limits and bitmap memory in official pages. The pool size, the window, the cache sizes, the budget order and the level text are my opinion. The native vs cross-platform guide covers shared modules.

Test yourself

In short: Eight questions from this case. Answer each one out loud first, then open the answer.

1. Why not one player per reel cell?

A fast swipe creates many players at once. Each holds a decoder, and a phone has only a few hardware decoders. When they run out, a new player fails or falls back to a slow software decoder, and the user sees a black screen. A pool of two or three players, owned outside the cells, avoids it.

2. How much of the next reels do you load?

A few seconds. About 5 seconds and a ready player for the next reel, 1 to 2 seconds of bytes for the one after, and only a poster for the rest. Whole-video preload wastes data when the user stops, and it competes with the reel on screen.

3. A signed video address changes every few minutes. What do you do with the cache key?

Build the key from the reel ID and the rendition, not from the address. Then bytes already on disk are still found after a new signed address.

4. Where does a pending upload live, and why?

In the app files folder, never in a cache folder. The system may clear a cache folder when storage is low, and the user must be able to see and cancel a pending upload.

5. The app is killed in the middle of an upload. How does it resume without a second copy?

The file, the post ID and the upload ID were written to the database before the first request. After the restart the app asks the server for the offset of the same upload ID and continues. A new ID would start a second copy.

6. What does a 12 megapixel photo cost in memory, and what do you decode instead?

About 48 MB: 4000 x 3000 x 4 bytes, whatever the file size. Decode a thumbnail at the cell size for the grid, a copy that fits the screen for the viewer, and tiles only for deep zoom.

7. The system is low on memory. What do you drop first?

The decoded neighbours of the viewer and other off-screen images, then the image memory cache, then shrink the player pool to one and stop preload. Never what is on screen. Disk caches stay because they cost no memory.

8. iOS: how does a video upload go on when the app is suspended?

A background URLSession does the transfer in another process. Uploads must come from a file, so each chunk is written to its own file. If the system ends the app, it relaunches it when events arrive, and the app creates the session again with the same identifier. On the next launch the app also reads its rows and asks the server for the offset.

Further reading (official docs)

I compared this page against these references to find gaps. I took ideas from them, not structure or wording:

  • The ByteByteGo mobile system design book and the DesignGurus mobile course
  • The Educative mobile course and the weeBox mobile system design repository
  • The Android Developers Blog posts on Media3 preloading
Words on this page (11)
Decode
To turn compressed video or image data into pixels that the screen can draw. It takes time, memory and battery.
Decoder
The part of the phone, often hardware, that decodes video. A phone has only a few at once.
Player pool
A small set of players that are made once and reused. Screens borrow one and give it back.
Poster
A still image of a video, shown until the first frame is drawn.
Prefetch
Loading data before the user asks for it.
Resumable upload
An upload that can continue from the byte where it stopped, because the server remembers how much it has.
Idempotency key
An ID the phone makes, so the server can tell that a repeated request is the same request.
State machine
A flow with a closed list of states and clear moves between them.
Tombstone
A small record that says an item was deleted, so an old copy cannot bring it back.
Remote flag
A setting that the server can change. It can turn a feature on or off without a new release.
Staged rollout
A release to a growing share of users, not to everyone at once.

Read the whole glossary

Verification notes (last reviewed 20 Oct 2026)

What I checked in official docs.

  • Media3 preloading (Android Developers Blog, part 1): DefaultPreloadManager takes the current index and asks for a target per item. The example loads 5 seconds of the next item and 3 seconds of the previous item. It says the manager and the player must come from the same builder so parts are shared. It does not describe a player pool.
  • Android user-initiated data transfer jobs: Android 14, setUserInitiated(true), a notification is required, and the app must be visible when scheduling. No Jetpack library supports them, and WorkManager with a foreground service is the fallback on 13 and lower. If the user stops the app, onStopJob() is not called.
  • Apple background URL sessions: transfers continue while the app is suspended, the system relaunches a terminated app, and only uploads from a file are supported. Recreate the session with the same identifier.
  • Apple BGContinuedProcessingTask: iOS 26 and later, shown as a Live Activity, can be cancelled by the person, and the system can end it under resource pressure.
  • Apple preferredForwardBufferDuration: a low value raises the chance of stalls and a high value uses more resources. The default of 0 lets the player choose.
  • Android bitmap guide: inSampleSize is rounded down to a power of two, and 2048 x 1536 with 4 bytes a pixel is about 12 MB.
  • Google Cloud Storage resumable uploads: a session lasts one week, chunks are multiples of 256 KiB, and an expired session answers 404 or 410. Amazon S3: parts of 5 MiB to 5 GiB, last part any size, up to 10,000 parts.
  • Media3 Transformer: trims and transcodes, uses MediaCodec for hardware encoding. Its page does not mention pause or resume.
  • Apple ImageIO: the thumbnail function exists since iOS 4. I did not read the page for its option keys.
  • My design, not from docs: the window sizes, the 150 ms settle wait, the pool of two or three, the order of the memory shrink, the cache sizes, the upload steps, the table of calls, removing location from photos and the level advice.
  • The numbers in sections 2, 4.1, 4.2 and 4.3 are assumptions or arithmetic that I recomputed: 2.5 Mbit/s is about 310 KB/s; 4000 x 3000 x 4 is 48 MB; 45 Mbit/s for 40 s is about 225 MB.
  • The follow-up questions are my reconstruction of challenges that public prep material reports. They are not transcripts of real rounds.

Keep reading

Get the free one-page cheat sheet

One page: the five steps, a 45-minute time plan and two checklists. Print it or keep it on your phone.

We email you the cheat sheet after you confirm your address. You can also open it now from the Start here page. You also get a short email each week with a follow-up round, and a note when a new case is published. Unsubscribe any time. Privacy.