0% found this document useful (0 votes)
3 views16 pages

Youtube Android System Design

Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views16 pages

Youtube Android System Design

Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Ac

System Design
#JetpackCompo #CleanArchitect #SoftwareEngin
#SystemDesign #AndroidDev se ure #Kotlin #ExoPlayer eering

When you tap a video on YouTube, a symphony of systems activates in milliseconds — from your Android device
to edge servers on every continent. This system design covers every architectural layer: the Jetpack Compose UI,
Clean Architecture domain logic, ExoPlayer adaptive streaming, Retrofit + gRPC network stack, Room caching,
WorkManager upload pipeline, Elasticsearch search, and the backend microservices serving over 1 billion hours
of video per day.

2.7B 500 80+ <


Monthly Active
Users hrs
Uploaded per
Minute
1B hrs PoPs 100ms
Watched Daily
CDN Edge Nodes Feed Load Target 6
Quality Tiers (ABR)

■■ Full System Architecture

YouTube Android — System Architecture

ANDROID CLIENT

Jetpack Compose ViewModel ExoPlayer Room DB WorkManager


UI Layer + StateFlow Media3 Local Cache Background

NETWORK & API GATEWAY

OkHttp Retrofit gRPC CDN FCM


Interceptors REST API Streaming Edge Cache Push

Layers: Client Network Services Storage arrows = data flow

BACKEND MICROSERVICES
Design Principle — Layered Architecture
Each layer Video
communicates only withAuth
the layer directly below it. The AndroidRecommendation
Search Client never calls backend storage directly
Transcoding
— all data Service
flows through the API Gateway,
Service which routes to the appropriate microservice.
Elasticsearch Service This separation means any
Workers
layer can be scaled, replaced, or deployed independently without cascading changes.

DATA STORAGE & CACHE

Google Cloud GCS Redis CDN


Bigtable Spanner Blob Store Cluster PoP Nodes

Page 1 of 16 · YouTube Android System Design


■■ Clean Architecture + MVVM on Android

YouTube's Android codebase follows Clean Architecture with strict layer separation. The Domain layer is pure
Kotlin — no Android SDK imports. The Data layer implements repository interfaces. The Presentation layer is
Jetpack Compose composables driven by ViewModels. This makes every layer independently testable and
replaceable.

PRESENTATION Jetpack Compose · ViewModels · Navigation · Coil Image Loading

INTERFACE ADAPTERS ViewModels · DTO Mappers · Repository Implementations · Room Entities

GetVideoFeedUseCase · PlayVideoUseCase · SearchVideosUseCase ·


APPLICATION (Use Cases) UploadVideoUseCase

DOMAIN ENTITIES (Zero


Android Imports) Video · User · Channel · Comment · Repository Interfaces · Business Rules

[Link] [Link]

// Hilt-injected ViewModel // Pure Kotlin — NO android.* imports


@HiltViewModel class GetVideoFeedUseCase
class VideoFeedViewModel @Inject @Inject constructor(
constructor( private val videoRepo:
private val getFeedUseCase: VideoRepository, // interface
GetVideoFeedUseCase private val prefsRepo:
) : ViewModel() { UserPrefsRepository
private val _state = ) {
MutableStateFlow(Loading) operator fun invoke():
val state = _state.asStateFlow() Flow>> = flow {
init { loadFeed() } val prefs = prefsRepo
private fun loadFeed() = .getPreferences()
[Link] { videoRepo
getFeedUseCase() .getPersonalisedFeed(
.catch { e -> userId = [Link],
_state.value = Error([Link]) region = [Link]
} )
.collect { result -> .map { videos ->
_state.value = when (result) { videos
is Success -> FeedUiState .filter {
.Success([Link]) ![Link](
is Error -> FeedUiState [Link])
.Error([Link]) }
} .sortedByDescending {
} [Link]
} }
} }
.collect { emit(Success(it)) }
}.catch { emit(Error(it)) }
}

[Link] — Interface (Domain) + Offline-First Implementation (Data)

// ■■■■■■■■ DOMAIN LAYER: interface only ■■■■■■■■

Page 2 of 16 · YouTube Android System Design


interface VideoRepository {
fun getPersonalisedFeed(userId: String, region: String): Flow>
suspend fun getVideoById(videoId: String): Video
fun searchVideos(query: String, filters: SearchFilters): Flow>
}
// ■■■■■■■■ DATA LAYER: implementation, bound in Hilt Module ■■■■■■■■
class VideoRepositoryImpl @Inject constructor(
private val remote: VideoRemoteDataSource, // Retrofit
private val local: VideoLocalDataSource, // Room DAO
private val network: NetworkMonitor
) : VideoRepository {
override fun getPersonalisedFeed(userId: String, region: String) = flow {
emitAll([Link](userId)) // 1. Emit Room cache immediately
if ([Link]) {
val fresh = [Link](userId, region)
[Link](fresh) // 2. Update Room cache
emit(fresh) // 3. Emit fresh network data
}
}
}

Page 3 of 16 · YouTube Android System Design


■ Jetpack Compose · Navigation · Hilt DI

[Link] [Link] + HiltModule


State-driven composable. When uiState changes in the ViewModel, Navigation Compose manages the back stack declaratively. Hilt's
Compose automatically re-renders only the affected subtree — no @InstallIn wires dependencies at compile time with zero runtime
manual invalidation needed. reflection overhead.

@Composable // ■■ Navigation ■■
fun VideoFeedScreen( @Composable
vm: VideoFeedViewModel fun AppNavGraph(
= hiltViewModel() nav: NavHostController
) { ) {
val state by [Link] NavHost(nav,
.collectAsStateWithLifecycle() startDestination =
when (state) { [Link]) {
Loading -> composable([Link]) {
ShimmerPlaceholder() VideoFeedScreen(
is Success -> onVideoClick = { id ->
VideoList( [Link](
videos = [Link], [Link]
onVideoClick = .createRoute(id))
vm::onVideoSelected }
) )
is Error -> }
ErrorRetryCard( composable(
onRetry = vm::refresh [Link],
) arguments = listOf(
} navArgument("id") {
} type =
@Composable [Link]
fun VideoList( }
videos: List, )
onVideoClick: (String) -> Unit ) { back ->
) { PlayerScreen(
LazyColumn( videoId = back
verticalArrangement = .arguments
[Link]([Link]) ?.getString("id")!!
) { )
// key prevents full recompose }
items(videos, }
key = { [Link] }) { v -> }
VideoCard( // ■■ Hilt Module ■■
video = v, @Module
onClick = { @InstallIn(SingletonComponent::class)
onVideoClick([Link]) object NetworkModule {
} @Provides @Singleton
) fun provideOkHttp(
} auth: AuthInterceptor
} ) = [Link]()
} .addInterceptor(auth)
.connectTimeout(10, SECONDS)
.readTimeout(30, SECONDS)
.build()
}

Page 4 of 16 · YouTube Android System Design


■ Feed Request Lifecycle
Compose Render
7
LazyColumn recomposes with new data

StateFlow Emit Feed Request Lifecycle — Offline-First Strategy


6
ViewModel emits Success to Compose UI

Upsert Cache
5
[Link]() — update DB

API Call
4
GET /v1/feed via Retrofit + OkHttp

Network Check
3
[Link]?

Room DB
2
Emit cached feed instantly (<5ms)

App Launch
1
User opens YouTube

Page 5 of 16 · YouTube Android System Design


■ Video Streaming — ExoPlayer · DASH · Adaptive Bitrate

YouTube uses ExoPlayer (Media3) with DASH (Dynamic Adaptive Streaming over HTTP). Videos are
pre-encoded into 6 quality tiers and split into 2–10 second segments stored in GCS. ExoPlayer probes available
bandwidth every second using an EWMA (Exponentially Weighted Moving Average) algorithm and switches
quality seamlessly mid-stream. The buffer strategy is tuned to minimise rebuffering while keeping memory usage
bounded.

ExoPlayer auto-switches quality based on real-time bandwidth probe (EWMA algorithm)


Adaptive Bitrate — Quality vs Bandwidth 40.0M
40Mbps

30Mbps

20Mbps
12.0M
10Mbps
5.0M
1.0M 2.5M
0.25M
144p 360p 480p 720p 1080p 2160p

Page 6 of 16 · YouTube Android System Design


[Link] [Link] — Compose + AndroidView

@HiltViewModel @Composable
class PlayerViewModel @Inject fun PlayerScreen(
constructor( videoId: String,
private val playUseCase: vm: PlayerViewModel
PlayVideoUseCase, = hiltViewModel()
@ApplicationContext ctx: Context ) {
) : ViewModel() { val player = [Link]
val player: ExoPlayer = LaunchedEffect(videoId) {
[Link](ctx) [Link](videoId)
.setLoadControl( }
[Link]() Column {
.setBufferDurationsMs( // ExoPlayer uses SurfaceView.
// Start after 15s buffered // Bridge it into Compose
minBufferMs = 15_000, // with AndroidView.
// Keep 50s ahead AndroidView(
maxBufferMs = 50_000, factory = { ctx ->
// Resume on 2.5s PlayerView(ctx).also {
bufferForPlaybackMs [Link] = player
= 2_500, [Link] = true
bufferForPlayback [Link] =
AfterRebufferMs RESIZE_MODE_FIT
= 5_000 }
).build() },
) modifier = Modifier
.setTrackSelector( .fillMaxWidth()
DefaultTrackSelector(ctx) .aspectRatio(16f/9f)
) )
.build() VideoMetadata(vm)
fun loadVideo(videoId: String) { CommentsSection(vm)
[Link] { RecommendedList(vm)
val manifest = }
playUseCase }
.getDashManifest(videoId)
[Link](
[Link]()
.setUri([Link])
.setMimeType(
MimeTypes.APPLICATION_MPD
).build()
)
[Link]()
[Link]()
}
}
override fun onCleared() =
[Link]()
}

Quality Bitrate Segment Size Segment Duration Use Case

2160p (4K) 35–45 Mbps ~140 MB/min 4s WiFi + Premium

1080p (FHD) 8–16 Mbps ~60 MB/min 4s WiFi / 5G

Page 7 of 16 · YouTube Android System Design


720p (HD) 2.5–5 Mbps ~20 MB/min 4s 4G LTE

480p (SD) 0.5–2 Mbps ~8 MB/min 2s 3G / Weak 4G

360p 0.3–1 Mbps ~4 MB/min 2s 2G / Data Saver

144p < 0.3 Mbps ~1 MB/min 2s Lowest bandwidth

Page 8 of 16 · YouTube Android System Design


■ Network Layer — Retrofit · gRPC · OkHttp Interceptors

[Link] — Retrofit [Link] + gRPC Chat


Suspend functions integrate with Coroutines. Used for feed, OkHttp intercepts every request for token refresh. gRPC handles
metadata, likes, search, comments. real-time live chat (bi-directional).

interface VideoApiService { // ■■ OkHttp Interceptor ■■


@GET("v1/feed") class AuthInterceptor @Inject
suspend fun getVideoFeed( constructor(
@Query("userId") userId: String, private val tm: TokenManager
@Query("pageToken") token: String?, ) : Interceptor {
@Query("region") region: String override fun intercept(
): Response chain: Chain
@POST("v1/videos/{id}/like") ): Response {
suspend fun likeVideo( var req = [Link]()
@Path("id") videoId: String .newBuilder()
): Response .addHeader("Authorization",
@GET("v1/search") "Bearer ${[Link]}")
suspend fun search( .build()
@Query("q") query: String, var res = [Link](req)
@Query("type") filter: String = "video" if ([Link] == 401) {
): Response // Refresh token
} val newTok =
[Link]()
[Link]()
req = [Link]()
.header("Authorization",
"Bearer $newTok")
.build()
res = [Link](req)
}
return res
}
}
// ■■ gRPC Live Chat ■■
class LiveChatSource @Inject
constructor(
private val stub:
LiveChatCoroutineStub
) {
fun streamMessages(
videoId: String
): Flow =
[Link](
ChatRequest {
[Link] = videoId
}
)
}

■ Caching Strategy — Multi-Layer Offline-First

Page 9 of 16 · YouTube Android System Design


Layer Technology What Is Stored Size / TTL Eviction

In-Memory Coil LruCache Decoded thumbnails 25% heap LRU

HTTP Cache OkHttp DiskLruCache REST API responses 100 MB / 7d LRU + TTL

DB Cache Room SQLite Feed, metadata, history Unbounded Stale-while-reval.

Video Segs ExoPlayer DASH video segments 2 GB LRU


SimpleCache

Offline DL WorkManager + Files Full video downloads Manual User delete

Redis Redis Cluster Hot metadata, session tokens Varies Sliding TTL 15m

CDN Edge Akamai / GCloud CDN Encoded segments at PoPs Per segment TTL + API purge

Room Database — VideoEntity + Flow-based DAO

@Entity(tableName = "videos")
data class VideoEntity(
@PrimaryKey val id: String,
val title: String,
val thumbnailUrl: String,
val durationSeconds: Int,
val viewCount: Long,
val channelId: String,
val recommendationScore: Float,
val cachedAt: Long = [Link]() // for TTL eviction
)
@Dao
interface VideoDao {
@Query("SELECT * FROM videos ORDER BY recommendationScore DESC")
fun getFeed(): Flow> // Room emits on EVERY DB write
@Upsert
suspend fun upsertVideos(videos: List)
@Query("DELETE FROM videos WHERE cachedAt < :threshold")
suspend fun evictStale(threshold: Long) // called periodically by CacheCleanupWorker
}

Page 10 of 16 · YouTube Android System Design


■ Video Upload Pipeline — WorkManager · TUS · Transcoding

Uploads use TUS (Tus Resumable Upload Protocol) over HTTP. The video file is split into 256 KB–5 MB chunks
sent as PATCH requests. The server responds with the new byte offset after each chunk, so on network failure the
upload resumes exactly where it stopped. WorkManager guarantees execution with battery/network-aware
constraints and automatic exponential backoff on retry.

Video Upload & Transcoding Pipeline


UPLOAD

Android App TUS Chunked API Gateway Cloud GCS Pub/Sub


Select Video Resumable Upload Auth + Routing Blob Storage Job Queue

TRANSCODE (parallel)

144p 360p 720p 1080p 2160p


Transcode Transcode Transcode Transcode (4K)

DELIVER

DASH Manifest CDN Push Signed URLs FCM Push


MPD Generation 80+ Edge PoPs Access Control Notify Uploader

1. Content Resolution
ContentResolver reads the video Uri from MediaStore. Validates MIME type (video/mp4, video/webm, video/mkv),
minimum duration, and file size. Returns a streaming InputStream for chunked reading.

2. WorkManager Enqueue
UploadWorker is enqueued with Constraints(requiresNetwork = CONNECTED, requiresStorageNotLow = true). Input
data carries videoUri, title, description, and privacy setting. The work request survives process death.

3. Initiate TUS Session


POST /upload/resumable — server returns an upload URL and upload-offset: 0. Client persists this URL to DataStore.
If the app restarts, upload resumes from the saved offset. Sessions are valid for 7 days.

4. Chunked Transfer (PATCH)


Each chunk is sent with Content-Type: application/offset+octet-stream and Upload-Offset header. Server responds
204 with the new offset on success. Client retries failed chunks with exponential backoff.

5. Transcoding Queue
GCS object-creation event triggers a Pub/Sub message. Cloud Tasks distributes jobs to an FFmpeg worker fleet. All 6
quality tiers (144p–4K) are encoded in parallel — typically ~90 seconds for a 10-minute video.

6. CDN Distribution & Notification


Encoded DASH segments are replicated to 80+ CDN PoPs globally via Akamai and Google Cloud CDN. Anycast DNS
routes each viewer to the nearest PoP. Firebase Cloud Messaging sends a push notification to the uploader.

[Link] — WorkManager with progress reporting

@HiltWorker
class UploadWorker @AssistedInject constructor(
@Assisted context: Context,

Page 11 of 16 · YouTube Android System Design


@Assisted params: WorkerParameters,
private val uploadRepository: UploadRepository
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val videoUri = [Link](KEY_VIDEO_URI) ?: return [Link]()
return try {
[Link](videoUri) { progress ->
setProgress(workDataOf(KEY_PROGRESS to progress)) // 0..100
}
[Link]()
} catch (e: IOException) {
[Link]() // network error — WorkManager applies exponential backoff
} catch (e: Exception) {
[Link]()
}
}
}

Page 12 of 16 · YouTube Android System Design


■ Search · Recommendations · Backend Services

Search — Paging 3 + Debounce Recommendation Engine


300ms debounce prevents flooding. Paging 3 RemoteMediator Two-tower neural network: user tower + video tower. ScaNN
keeps Room DB and network results in sync. Elasticsearch (approx nearest-neighbour) retrieves 500 candidates. Deep ranking
backend with inverted index + word2vec query expansion. DNN scores and ranks. ~1M QPS cluster.

// [Link] // Two-stage pipeline:


val results: Flow> = //
searchQuery // Stage 1 — Retrieval
.debounce(300) // User embedding (watch history,
.distinctUntilChanged() // likes, searches) ->
.flatMapLatest { query -> // ScaNN ANN index ->
Pager( // ~500 candidates
config = PagingConfig( //
pageSize = 20, // Stage 2 — Ranking
prefetchDistance = 5 // Deep ranking DNN scores
), // each candidate on:
remoteMediator = // click prob, watch-time pred,
SearchRemoteMediator( // diversity, freshness ->
query, apiService, // Top-N returned
searchDao // Client API call:
) interface RecommendationApi {
) { @GET("v1/recommended")
[Link](query) suspend fun getNextUp(
}.flow @Query("videoId") videoId:
} String,
.cachedIn(viewModelScope) @Query("count") count:
Int = 15
): List
}
// Redis cache: 15 min TTL
// ~1M QPS, served via gRPC

■■ Backend Microservices — Scale & Responsibility

Service Language Scale Responsibility

VideoService Go ~500K RPS Video CRUD. View count batched via Pub/Sub to avoid Bigtable
hot-row contention

AuthService Python ~200K RPS OAuth 2.0 + Google Sign-In. JWT (1h TTL). Refresh tokens in
Spanner. Rate-limiting per device

SearchService Java ~300K RPS Elasticsearch cluster. Inverted index. BM25 scoring. word2vec query
expansion. Spell correction

RecommendationServi C++ / Py ~1M RPS Two-tower DNN. ScaNN ANN. TF Serving for ranking. Redis
ce response cache (15 min TTL)

TranscodingWorkers C++ Async FFmpeg on GCE VMs. Cloud Tasks queue. Parallel 6-quality
fleet encode. ~90s per 10-min video

Page 13 of 16 · YouTube Android System Design


NotificationService Go ~100K/s FCM push via Firebase Admin SDK. Server-side subscriber fan-out
per channel upload

CDN / Edge Nginx 80+ PoPs Akamai + Cloud CDN. Anycast DNS. Signed URLs for premium.
~40ms avg segment latency

Page 14 of 16 · YouTube Android System Design


■ Performance Optimisations

Compose Stability Annotations


Mark data classes @Stable or @Immutable. Use remember{} and derivedStateOf{} to skip unnecessary
recompositions. Profile with Layout Inspector's recomposition highlighting.

Baseline Profiles
Pre-compile hot code paths (VideoFeedScreen, PlayerScreen) with the Macrobenchmark library. Reduces JIT
compilation overhead by ~30% on cold start.

LazyColumn Stable Keys


Always provide stable keys: items(videos, key = { [Link] }). Without keys, the entire list recomposes on any data change
— critical for YouTube's large feeds.

Coil 3 Image Loading


Async thumbnail loading with crossfade(true), shimmer placeholders, and background pre-fetching via
FeedPreFetcher coroutine before the user scrolls to each item.

R8 Full Mode + Baseline


Full R8 minification + class merging + code shrinking. Careful ProGuard keep-rules for Retrofit, Room, Hilt. Combined
with Baseline Profiles: ~40% APK size reduction.

App Startup Library


Defer non-critical initialisation (Analytics, Crashlytics) to post-first-frame using Jetpack App Startup. WorkManager
initialised lazily. Saves ~200ms time-to-interactive.

■■ Key Design Trade-offs

Decision Chosen Approach Alternative Why This Way

UI Toolkit Jetpack Compose XML + Views Declarative state, less boilerplate, native Kotlin

Async Coroutines + Flow RxJava Structured concurrency, built-in cancellation,


simpler syntax

DI Hilt Manual Dagger Compile-time safety, standard components, less


setup

Real-time gRPC WebSocket Binary efficiency, auto-generated stubs,


bidirectional streaming

Local DB Room Raw SQLite Type-safe DAOs, Flow integration, migration


support

Background Work WorkManager Foreground Service Battery-aware, guaranteed execution, survives


process death

Pagination Paging 3 Manual pages Load states, retry, RemoteMediator pattern built-in

Page 15 of 16 · YouTube Android System Design


Save this for your next System Design interview! Follow for weekly Android + Backend
Architecture deep dives. What would you design differently? Comment below.

Page 16 of 16 · YouTube Android System Design

You might also like