Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Monorepo at Scale: How We Slashed Build Times b...

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →

Monorepo at Scale: How We Slashed Build Times by 75%

As Android teams grow, monorepos often become a bottleneck—leading to tangled modules, bloated dependency graphs, and slow build times. Optimizing developer efficiency at scale requires fundamental structural changes.

This talk is about how we maintain a high-performance Android monorepo for a team of 20+ developers. We will break down the architectural and tooling strategies required to keep compilation fast and developers productive, focusing on two core pillars:

• Module Structure: Leverage JVM module type/ Practice Dependency Inversion/ Custom IJ plugin to create new modules/ Optimizing incremental compilation by strictly enforcing module boundaries.
• Dependency Injection: Aggregation style of DI wiring/ DI's build tools comparison/ Moving away from KAPT and KSP toward Kotlin Compiler Plugins

This talk is designed for Android developers looking to optimize their projects. Whether you are heavily modularizing your codebase or have just started, you will find valuable insights here. For platform engineers, Android leads, or DI enthusiasts, you will leave with concrete, actionable takeaways to implement in your own projects!

Avatar for Kevin Chiu

Kevin Chiu

October 16, 2026

Other Decks in Programming

Transcript

  1. Monorepo at Scale How We Slashed Build Times by 75%

    Kevin Chiu Android Team Lead @BandLab
  2. Project Scale • Monorepo of 1,332 modules • 350 JVM

    modules (26%) • Android module for the rest • Local build time last 7 days (:app:assembleDebug) • Median 20s, Mean 48s (821 samples) : • P25-75 14-48s
  3. JVM vs Android Module • Assemble a JVM module: 7

    tasks • Assemble an Android module: 30 tasks plugins { id("org.jetbrains.kotlin.jvm") } plugins { id("com.android.library") }
  4. JVM vs Android Module Partial JVM Delta 📉 Clean build

    (Conf guration) 66.35s 47.75s -28.03% Clean build (Execution) 286s 284s -0.7% Tasks count 34,461 28,210 -18.14% i All Android
  5. Dependency Inversion :user prof le :band prof le :post i

    - i - - - :following feed :trending feed
  6. Dependency Inversion :post interface PostApiService { @GET("posts/{id}") suspend fun getPost(@Path("id")

    postId: String) } : : class PostRepository( private val postApiService: PostApiService, ) { suspend fun getPost(postId: String) Post { return postApiService.getPost(postId) } } Post
  7. Dependency Inversion :post interface PostApiService { @GET("posts/{id}") suspend fun getPost(@Path("id")

    postId: String) } : : class PostRepository( private val postApiService: PostApiService, private val tracker: Tracker, ) { suspend fun getPost(postId: String) Post { tracker.track("post_requested") return postApiService.getPost(postId) } } Post
  8. Dependency Inversion :user prof le :band prof le :post -

    i i - - - :following feed :trending feed
  9. Dependency Inversion :post interface PostApiService { @GET("posts/{id}") suspend fun getPost(@Path("id")

    postId: String) } : : class PostRepository( private val postApiService: PostApiService, ) { suspend fun getPost(postId: String) Post { return postApiService.getPost(postId) } }n Post
  10. Dependency Inversion :post:api :post:impl interface PostApiService { @GET("posts/{id}") suspend fun

    getPost(@Path("id") postId: String) } : : : interface PostRepository { suspend fun getPost(postId: String) } Post class PostRepositoryImpl( private val postApiService: PostApiService, ) : PostRepository { override suspend fun getPost(postId: String) return postApiService.getPost(postId) } }n Post Post {
  11. Dependency Inversion :post:api :post:impl interface PostApiService { @GET("posts/{id}") suspend fun

    getPost(@Path("id") postId: String) } : : : interface PostRepository { suspend fun getPost(postId: String) } Post class PostRepositoryImpl( private val postApiService: PostApiService, private val tracker: Tracker, ) : PostRepository { override suspend fun getPost(postId: String) tracker.track("post_requested") return postApiService.getPost(postId) } } Post Post {
  12. Dependency Inversion :user prof le :band prof le :post:api :trending

    feed :following feed - i i - - - :post:impl
  13. Module Creation Plugin • Limit module to :api, :impl, :screen

    and :ui • Apply convention plugins • Screen templates • Wire the module to top level :app - - github.com/bandlab/bandlab android intellij plugin - / / • https:
  14. Module Creation Plugin :screen :impl Business logic Screen container Domain

    impl Shared VM impl :api :ui Domain model Abstraction Composable UI states
  15. Module Creation Plugin :screen :impl Business logic Screen container Domain

    impl Shared VM impl :api :ui Domain model Abstraction JVM Composable UI states
  16. Project Doctor projectDoctor { errorOn(":api should be a JVM module")

    { projectWith { nameEndsWith(":api") } .thatHasPluginWith { id("com.android.library") } } errorOn(":api cannot depends on :impl") { projectWith { nameEndsWith(":api") } .thatHasDependencyWith { nameEndsWith(":impl") } } }n
  17. Project Doctor • Gradle DSL to enforce the desired module

    structure • Isolated project compatible - github.com/bandlab/gradle project doctor - / / • https:
  18. Manual Wiring (Dagger) :user:api api :user:impl UserRepo UserRepoImpl : :

    : / / @Module abstract class UserRepoModule { @Binds abstract fun bindUserRepo(impl: UserRepoImpl) } UserRepo implementation :app AppGraph @Component( modules = [ tons of modules… UserRepoModule class, ], ) interface AppGraph
  19. Aggregation (Anvil) :user:api UserRepo api :user:impl UserRepoImpl : : :

    @ContributesBinding(AppScope class) class UserRepoImpl : UserRepo : implementation @MergeComponent(AppScope interface AppGraph :app AppGraph class)
  20. Aggregation (Anvil) :user:api UserRepo api :user:impl UserRepoImpl @ContributesBinding(AppScope class) class

    UserRepoImpl : UserRepo : : fun com_example_AppScope(contributed: UserRepoImpl) { return error("Never called") } : :app AppGraph @MergeComponent(AppScope interface AppGraph class) classpath scanning package hints : implementation
  21. DI’s Build Tool • Dagger, Hilt: KAPT/ KSP • Kotlin

    inject: KSP • Koin: KSP/ Kotlin Compiler Plugin • Metro: Kotlin Compiler Plugin Note - : : • KAPT Kotlin Annotation Processing Tool • KSP Kotlin Symbol Processing
  22. 🚇 Metro • Compile time DI framework • Kotlin Ergonomic,

    Coroutine support • Anvil style Aggregation - / / - • https: zacsweers.github.io/metro/
  23. Dagger android class MyActivity: AppCompatActivity() @Modulen interface MyActivityInjector { @ContributesAndroidInjector(modules

    = [MyActivityModule fun myActivity() MyActivity class]) }n @Module object MyActivityModule { @Provides fun provideLifecycle(activity: MyActivity) Lifecycle = activity.lifecycle @Provides fun provideCoroutineScope(activity: MyActivity) CoroutineScope = activity.lifecycleScope } : : : : - : 2017 Aug
  24. @ContributesInjector class MyActivity: AppCompatActivity() We generate this with Anvil Compiler

    API @MergeSubcomponent interface MyActivitySubcomponent { @Subcomponent.Factory interface Factory { fun create() MyActivitySubcomponent } } : : 2023 Dec / / Dagger + Anvil (K1)
  25. @ContributesInjector class MyActivity: AppCompatActivity() We generate this with Anvil Compiler

    API @MergeSubcomponent( modules = [DefaultActivityDependencies class] ) interface MyActivitySubcomponent { @Subcomponent.Factory interface Factory { fun create() MyActivitySubcomponent } @Module object DefaultActivityDependencies { @Provides fun provideLifecycle() Lifecycle @Provides fun provideCoroutineScope() CoroutineScope More default deps } } : : : : : : / / 2023 Dec / / Dagger + Anvil (K1)
  26. @ContributesInjector class MyActivity: AppCompatActivity() We generate this with KSP @MergeSubcomponent(

    modules = [DefaultActivityDependencies ) interface MyActivitySubcomponent { class] @Subcomponent.Factory interface Factory { fun create() MyActivitySubcomponent } } : : : : 2024 Sep / / Dagger + Anvil-KSP (K2)
  27. @ContributesInjector class MyActivity: AppCompatActivity() We generate this with KSP @GraphExtension(

    bindingContainers = [DefaultActivityDependencies ) interface MyActivityGraphExtension { class] @ContributesTo(AppScope class)n @GraphExtension.Factory interface Factory { fun create() MyActivityGraphExtension } } : : : : : : 2025 Jul / / Metro + KSP
  28. Metro + Metro Station @StationEntry class MyActivity: AppCompatActivity() { We

    generate this with custom compiler plugin @GraphExtension( bindingContainers = [DefaultActivityDependencies ) interface FeatureExtension { class] @ContributesTo(AppScope class) @GraphExtension.Factory interface Factory { fun create() FeatureExtension } } } : : : : : / / 2026 Jun
  29. Metro Station • Built with Metro Compiler Extension API •

    Generate code for Metro to process • One :compileKotlin task, no KSP, no KAPT github.com/bandlab/metro station - / / • https:
  30. Metro Station @StationEntry class ExampleService : Service() { @Inject lateinit

    var exampleClass: ExampleClass override fun onCreate() { super.onCreate() exampleClass.doSomething() } }n
  31. Metro Station @StationEntry class ExampleService : Service() { @Inject lateinit

    var exampleClass: ExampleClass override fun onCreate() { super.onCreate() exampleClass.doSomething() } Generated @GraphExtension interface FeatureExtension { @ContributesTo(AppScope class) @GraphExtension.Factory interface Factory { fun create() FeatureExtension } } : : : / / }n
  32. Metro Station @StationEntry class ExampleService : Service() { @Inject lateinit

    var exampleClass: ExampleClass override fun onCreate() { val factory = this.resolveFactory<FeatureExtension.Factory>() factory.create().inject(this) super.onCreate() exampleClass.doSomething() } Generated @GraphExtension interface FeatureExtension { fun inject(service: ExampleService) @ContributesTo(AppScope class) @GraphExtension.Factory interface Factory { fun create() FeatureExtension } } : : : / / }n Note: resolveFactory casts the AppGraph via Context to the contributed factory type
  33. Metro Station @StationEntry class ExampleBroadcastReceiver : BroadcastReceiver() { @Inject lateinit

    var exampleClass: ExampleClass override fun onReceive(context: Context, intent: Intent) { exampleClass.doSomething() } } Note: resolveFactory casts the AppGraph via Context to the contributed factory type
  34. Metro Station @StationEntry class ExampleBroadcastReceiver : BroadcastReceiver() { @Inject lateinit

    var exampleClass: ExampleClass override fun onReceive(context: Context, intent: Intent) { val factory = context.resolveFactory<FeatureExtension.Factory>() factory.create().inject(this) exampleClass.doSomething() } } Note: resolveFactory casts the AppGraph via Context to the contributed factory type
  35. Metro Station @StationEntry class ExampleWorker( context: Context, params: WorkerParameters, )

    : CoroutineWorker(context, params) { @Inject lateinit var exampleClass: ExampleClass override suspend fun doWork() Result { exampleClass.doSomething() return Result.success() } } Note: resolveFactory casts the AppGraph via : Context to the contributed factory type
  36. Metro Station @StationEntry class ExampleWorker( context: Context, params: WorkerParameters, )

    : CoroutineWorker(context, params) { @Inject lateinit var exampleClass: ExampleClass override suspend fun doWork() Result { val factory = context.resolveFactory<FeatureExtension.Factory>() factory.create().inject(this) exampleClass.doSomething() return Result.success() } } Note: resolveFactory casts the AppGraph via : Context to the contributed factory type
  37. FIR vs IR • FIR represents the structure of Kotlin

    code • IR represents the semantics of Kotlin code
  38. FIR vs IR • FIR represents the structure of Kotlin

    code • IR represents the semantics of Kotlin code : class HelloDroidcon { fun greet() Boolean { println("Hello, Lisbon!") return true } }
  39. - Mutation Metro + KSP Metro + Metro Station Root

    Abi 54.79s 28.72s -47.57% Root Non abi 40.26s 17.14s -57.43% Delta 📉
  40. - Mutation Dagger + Anvil KSP Metro + Metro Station

    Delta 📉 Root Abi 115.02s 28.72s -75.03% Root Non abi 77.70s 17.14s -77.94% 2025 Jun 2026 Jun
  41. Key Takeaways • Convert Android modules to JVM • Practice

    dependency inversion • Enforce module structure that scales well • Avoid KAPT and KSP if you can