Youtube Android System Design
Youtube Android System Design
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.
ANDROID CLIENT
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.
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.
[Link] [Link]
@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()
}
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
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.
30Mbps
20Mbps
12.0M
10Mbps
5.0M
1.0M 2.5M
0.25M
144p 360p 480p 720p 1080p 2160p
@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]()
}
HTTP Cache OkHttp DiskLruCache REST API responses 100 MB / 7d LRU + TTL
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
@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
}
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.
TRANSCODE (parallel)
DELIVER
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.
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.
@HiltWorker
class UploadWorker @AssistedInject constructor(
@Assisted context: Context,
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
CDN / Edge Nginx 80+ PoPs Akamai + Cloud CDN. Anycast DNS. Signed URLs for premium.
~40ms avg segment latency
Baseline Profiles
Pre-compile hot code paths (VideoFeedScreen, PlayerScreen) with the Macrobenchmark library. Reduces JIT
compilation overhead by ~30% on cold start.
UI Toolkit Jetpack Compose XML + Views Declarative state, less boilerplate, native Kotlin
Pagination Paging 3 Manual pages Load states, retry, RemoteMediator pattern built-in