Skip to content
All essays

Mobile architecture

Short video feed system design interview: a vertical video feed, from the hiring side

"Design a short video feed" sounds like a ranking question. On mobile it is a resource question: how many videos you prepare, how many players exist, what the cache throws away, and what the phone does when it gets hot. A worked answer from the hiring side.

13 min read

Ask a room of mobile engineers to design a vertical video feed, the kind TikTok made familiar, and most of them draw a recommendation service, a CDN and a list view within five minutes. Then I ask: the user swipes, and the next video must show a moving frame almost at once. How many videos are already loaded at that moment, and what did each of them cost the phone?

In this prompt the ranking is not the hard part: the server ranks. What 500+ technical interviews from the hiring side have taught me to listen for is the client side of the bill. The client decides how much of the future it pays for in advance, in bytes, decoders, memory and heat. That is what the round scores.

This is a worked answer for iOS and Android. For how mobile design differs from backend design in general, see the essay on that difference. For a smaller pipeline with the same cache questions, see the image loading answer.

  • The prompt: design the client of a full-screen vertical video feed, swipe to advance, autoplay, likes and comments, a heavy and young audience on mixed devices.
  • The shape: budgets, the window, the player pool, cache and eviction, bitrate, analytics, offline, memory and heat, iOS versus Android, rollout.
  • The test underneath: can you put a number on what each preloaded video costs, and say who decides to spend it.

The 45-minute round, minute by minute

MinutesWhat you doWhat the interviewer is checking
0 to 5Clarify devices, networks, autoplay, sound, offlineDo you ask before you build?
5 to 10Budgets: first frame, stalls, memory, heat, dataDo you design to numbers?
10 to 22The window and the player poolDo you know what a decoder costs?
22 to 30Cache, eviction and bitrateWhat does the app throw away, and when?
30 to 38Analytics, offline, memory and thermal responseWhat happens on the phone nobody tested?
38 to 45iOS versus Android, metrics, rolloutHave you shipped media before?

Candidates who spend fifteen minutes on the ranking service have answered a backend question. Name the feed API in one sentence: pages of item ids with a cursor, each item carrying a stream URL, a poster image and its duration. Then move to the device.

Budgets before boxes

The first ten minutes earn the most points when you turn "smooth" into numbers you can test. Name them per device tier, at a percentile, never as an average from your own phone.

BudgetWhat it measuresWhy it shapes the design
Time to first frame after a swipeSwipe end to the first rendered video frame, p95Decides how far ahead you prepare
Stall rateRebuffer events per minute of playbackDecides how deep the buffer runs and how low the first bitrate is
Memory high-water markPeak resident memory during a ten-minute scrollDecides how many players can exist
Heat and batteryThermal state reached in a long sessionDecides when quality steps down
Data spent on unwatched videoBytes prefetched for items the user never reachedDecides the window on cellular

The last row is the one strong candidates add unprompted. A feed that prefetches aggressively looks instant in a demo, and in the field it spends a user's data plan on videos they swiped past in half a second. Wasted prefetch is a budget too, and someone has to own it.

The window: bytes, prepared players and the one on screen

The structural move interviewers wait for is separating three levels of readiness. They cost very different amounts, so they get different window sizes.

  • Playing: the visible item, decoding, with audio.
  • Prepared: the next item, and the previous one for a swipe back, loaded into a player with the first frame decoded and paused. This is what makes a swipe feel instant. It holds a decoder and buffers, so the count is small.
  • Prefetched: the next few items as bytes only, the first seconds of each written to the disk cache. No decoder, no surface. Cheap in memory, not free in data.

Prefetch only the start of each video. Most swipes happen in the first seconds, so downloading the full file of an item three positions ahead buys almost nothing and spends the most. When the user settles on an item, its buffer grows; when they swipe fast, cancel the prefetches that fell behind, so the network serves the item on screen first.

Then say how the window changes. On Wi-Fi with a charged phone it can run a few items ahead. On cellular, in low power mode, under memory pressure or when the device is hot, it shrinks, down to preparing only the next item. The window is a policy with inputs, not a constant in the view controller.

The player pool: decoders are the scarce resource

A candidate who creates one player per cell in a list has not shipped video. Hardware decoders are limited per device, each player holds buffers and a rendering surface, and creating players is slow. The answer is a small pool, reused as the user scrolls.

Swift
@MainActor
final class PlayerPool {
    private var idle: [AVPlayer] = []
    private var inUse: [Int: AVPlayer] = [:]   // feed index -> player

    init(capacity: Int = 3) {
        idle = (0..<capacity).map { _ in AVPlayer() }
    }

    func attach(_ url: URL, at index: Int, current: Int) {
        if inUse[index] != nil { return }
        if idle.isEmpty, let far = inUse.keys.max(by: { abs($0 - current) < abs($1 - current) }) {
            release(far)                       // recycle the item furthest from the screen
        }
        guard let player = idle.popLast() else { return }
        let item = AVPlayerItem(url: url)
        item.preferredForwardBufferDuration = index == current ? 0 : 2   // neighbours buffer a little
        player.replaceCurrentItem(with: item)
        inUse[index] = player
    }

    func release(_ index: Int) {
        guard let player = inUse.removeValue(forKey: index) else { return }
        player.pause()
        player.replaceCurrentItem(with: nil)
        idle.append(player)
    }
}

Two details to say out loud. A preferredForwardBufferDuration of zero means the system chooses, which is right for the item on screen; a short explicit value keeps neighbours from filling memory with video nobody is watching yet. And the pool size is the prepared window plus one, decided by the device tier, so a low-end phone may run two players where a flagship runs three.

The invariant worth stating: no more players exist than the pool allows, whatever the scroll speed. A fling through forty items must recycle, never allocate.

Cache and eviction: what you throw away first

Video needs its own disk cache, sized in bytes, separate from the image cache for posters and avatars. Mixing them lets one long video push out every thumbnail on screen.

Eviction order is the decision. Least recently used is the default and it is close, but a feed knows more than recency: evict watched before unwatched, and the far past before the near future. An item the user finished is unlikely to be needed again; a prefetched item two positions ahead is about to be. Give the cache a ceiling per item and a total ceiling, and make the total smaller on devices that report low free storage, because the user and the OS can both delete your files.

One more rule: a cached segment is keyed by the exact rendition, not just the video id. Otherwise a quality change mid-session can mix renditions in one playback, or a reused cache entry plays a resolution the current policy would never choose.

Bandwidth adaptation: start low, earn the climb

Serve adaptive streams, HLS on iOS and HLS or DASH on Android, with a ladder of renditions. The player's adaptive logic picks a rendition as playback goes. Your design decides the start and the caps.

Start low. The first frame matters more than the first second's sharpness, and a short video may be over before a high starting bitrate would have paid off. Use the measured throughput from recent segments, not the network type label: a strong cellular link can beat a congested Wi-Fi. Cap the peak on cellular when the user chose data saving, and cap it by screen size, since a small phone gains nothing from the top rendition. On iOS that cap is preferredPeakBitRate on the player item; on Android it is a track selection constraint on the player.

Then name the coupling. A higher bitrate costs more decoding work and more radio time, which costs heat and battery. Bitrate policy and thermal policy are one system, and treating them separately is how the phone overheats while every metric for video quality looks excellent.

Analytics: events that survive the process

Watch events are not decoration in this product. They feed ranking, so a lost or reordered event degrades every future page. The round tests whether you treat them as data with a contract.

  • Persist before sending. Write each event to a local queue first, then upload in batches. The process will be killed mid-scroll, and an in-memory buffer loses exactly the last minute of a session, which is the minute ranking most wants.
  • Give each event an id and a sequence number per session. The server deduplicates by id, so retries are safe, and orders by sequence, not by device clock or arrival time. Batches can arrive out of order after a reconnect.
  • Measure watch time from playback, not from the screen. Count only the time the player was rendering frames, minus stalls. A paused video on screen is not a watched video.
  • Send impressions with the cursor of the page they came from, so ranking can tell which page produced which behaviour.

Upload on a timer and on backgrounding, in small batches, and let the OS scheduler handle uploads that wait for a network. Analytics must never compete with the video on screen for bandwidth.

The offline story: say what the product promises

A strong answer does not pretend a streaming feed works offline. It says what offline means for this product. When the connection drops, the user can keep watching what is already cached in the window, then sees a clear state, not an endless spinner. Likes and follows made offline go into an outbox and sync later, idempotently, the same discipline as the offline sync answer.

If the product adds downloads for later, that is a different feature with a different contract: a download state machine, a storage budget and licences that must survive an app update. Name it as a separate scope, so the interviewer knows you see the size of it.

Memory and heat: the phone nobody tested

Memory is the first wall. Every prepared player holds decoded frames and buffers, posters sit in an image cache, and the feed keeps growing as the user scrolls. Respond to the platform's warnings before the OS acts: on iOS the memory warning, on Android onTrimMemory. Shrink in a fixed order: drop the prepared previous item, then prefetched bytes held in memory, then the image cache. Trim the feed model itself, keeping ids and cursors for items far behind and dropping their heavy metadata.

Heat is the second wall, and the one candidates miss. A feed is a long session of decoding, network and screen at full brightness. Read the system's thermal signal and step down on a ladder:

Swift
enum Tier { case full, reduced, minimal }

func tier(for state: ProcessInfo.ThermalState, lowPower: Bool) -> Tier {
    switch state {
    case .nominal, .fair: return lowPower ? .reduced : .full
    case .serious: return .reduced
    case .critical: return .minimal
    @unknown default: return .reduced
    }
}

Each tier is a set of choices: the window size, the peak bitrate, the pool size. Respond fast when the signal rises and recover slowly, or quality jumps up and down every time the phone warms. And record every step down as an event, segmented by device tier. If the client lowers quality and nobody can see it, a whole cohort of older phones can watch at the floor for weeks with every dashboard green.

What changes on iOS and Android

ConcerniOSAndroid
PlayerAVPlayer with AVPlayerItem, a pool you manageMedia3 ExoPlayer, a pool you manage, and Media3's preload support for feeds
Disk cache for streamed videoNot built in for streamed HLS; a resource loader or a local proxy writes segmentsMedia3 SimpleCache with an evictor, through a caching data source
Buffer controlpreferredForwardBufferDuration, preferredPeakBitRateLoad control buffer settings, track selection constraints
Memory signalMemory warningonTrimMemory levels
Thermal signalProcessInfo thermalState and its change notificationThermal status listener; thermal headroom forecast on API 30 and later
Device spreadFewer models, still a real low endWide spread in decoders, memory and thermal behaviour

The Android row that matters most is the last one. Decoder limits and thermal behaviour vary by chip and by vendor, so the pool size and window are chosen per device tier, and the tier list is data from the field, not a guess from the office drawer of test phones.

Metrics and rollout

Name the numbers: time to first frame after a swipe at p50 and p95, stalls per minute, the share of prefetched bytes never played, peak memory in a ten-minute session, the share of sessions reaching a serious thermal state, and the share of time spent at each quality tier. Segment every one by device tier and OS version, because the average is set by the newest phones.

Change the window size, pool size and bitrate caps behind remote configuration, by device tier, so a bad value can be turned back without an app release. Roll a new player stack to a small share first and compare the five numbers above against the old one before widening it.

Mid, senior and staff: what the answers sound like

TopicMid-levelSeniorStaff
Preloading"Preload the next few videos."Separates prepared players from prefetched bytes; window shrinks on cellular and under pressure.Budgets wasted prefetch per tier and names who owns that number.
Players"A player in each cell."A small reused pool, sized by device tier.States the invariant as a test: a fling never allocates past the pool.
Quality"Adaptive bitrate handles it."Starts low, caps by screen and data mode, couples bitrate with heat.Makes every forced step down visible telemetry, segmented by tier.
Events"Send an event on each view."Persisted queue, ids, sequence numbers, watch time from playback.Treats events as a ranking contract with a loss metric and an owner.

The staff column is not more components. It is the same design with budgets, owners and a way to know when it is wrong on a phone nobody in the team carries.

The follow-ups interviewers use to push

  • "The user swipes through twenty videos in five seconds." Tests cancellation and the pool invariant.
  • "It is a three-year-old Android phone." Tests budgets per device tier and decoder limits.
  • "The user is on a metered plan." Tests the window as a policy and wasted prefetch.
  • "After ten minutes the phone is hot." Tests the thermal ladder and its telemetry.
  • "The app is killed mid-scroll." Tests the persisted event queue.
  • "The train goes into a tunnel." Tests the offline story and the outbox.

Prepare one sentence for each. If you want more prompts in this shape to rehearse, the mobile system design prompt pack is a free set. If a follow-up makes you add a box, your design was missing a decision. If it makes you point at a box that already handles it, you are having the conversation the round is for.

Questions engineers ask about video feed interviews

How do you answer "design a short video feed" in a mobile system design interview?

Set budgets first: time to first frame, a stall rate, memory and heat on a low-end device. Then design the window around the visible item: which neighbours are prefetched as bytes, which are prepared in a player, and how many players exist at all. Add a disk cache with an eviction order, a bitrate policy that starts low, analytics that survive the process being killed, and say what changes on iOS and Android.

How many videos should the app preload?

Fewer than candidates expect. Prepare the next item in a decoder-backed player, prefetch the first seconds of the next few as bytes, and keep the previous item warm for a swipe back. The number is a budget per device tier, not a constant, and it shrinks under memory pressure, thermal pressure and cellular data limits.

Should the interview answer go deep on the ranking algorithm?

No. Say the server ranks and returns pages with a cursor, and that the client reports what was watched. The mobile round is about what the device does with the page: playback start, memory, heat, data use and the events that feed ranking back.

How should I practise this round?

Answer out loud against a 45-minute timer, then change one fact: the phone is three years old, the user is on a metered plan, or the app is killed mid-scroll. Answer again. The second pass shows which numbers in your design were decisions and which were guesses.

Share this essay