androidengineers.Book a session

App Modularization

Dependency Graph & API Contracts

article20 minHard

A well-designed module dependency graph is the backbone of a maintainable multi-module project. It determines build performance, team independence, and how changes propagate through the codebase.

The DAG Requirement

Module dependencies must form a Directed Acyclic Graph (DAG). Cycles break Gradle's incremental build and cause compilation failures:

❌ Cycle: :feature-a → :feature-b → :feature-a

✅ DAG:
:app
├── :feature-feed    (:feed)
├── :feature-profile (:profile)
│
:feed → :domain-articles → :data-articles → :core-network
                         ↘ :core-database
:profile → :domain-users → :data-users → :core-network

Defining Public API Contracts

Each module should expose a minimal, stable public API. Internal implementation details stay internal:

// :domain-articles module
// Public API — other modules depend on these
interface ArticleRepository {
    fun getArticles(): Flow<List<Article>>
    suspend fun getArticle(id: String): Article?
    suspend fun saveArticle(article: Article)
}

data class Article(
    val id: String,
    val title: String,
    val body: String,
    val publishedAt: Long
)

// Internal — :data-articles implements this
internal class ArticleRepositoryImpl(
    private val localDataSource: ArticleLocalDataSource,
    private val remoteDataSource: ArticleRemoteDataSource
) : ArticleRepository { ... }

API Modules Pattern

For large teams, separate the public contract from the implementation:

:data:articles:api      ← interfaces + domain models only
:data:articles:impl     ← implementation of :data:articles:api
// :data:articles:api/build.gradle.kts
dependencies {
    // No implementation dependencies — pure interfaces
    api(project(":core:common"))  // shared models
}

// :data:articles:impl/build.gradle.kts
dependencies {
    implementation(project(":data:articles:api"))
    implementation(project(":core:network"))
    implementation(project(":core:database"))
}
// :app wires them together at DI time
@Module
@InstallIn(SingletonComponent::class)
object ArticlesModule {
    @Provides
    @Singleton
    fun provideArticleRepository(impl: ArticleRepositoryImpl): ArticleRepository = impl
}

Features only depend on :data:articles:api — they never see the implementation.

Visualizing the Dependency Graph

# Generate a dependency graph with Gradle
./gradlew :app:dependencies --configuration debugRuntimeClasspath

For visual graphs, use the com.jakewharton.gradle.dependencies plugin or dependency-graph-generator:

// build.gradle.kts (root)
plugins {
    id("com.vanniktech.dependency.graph.generator") version "0.8.0"
}
./gradlew generateDependencyGraph
# Outputs: build/reports/dependency-graph/dependency-graph.svg

Detecting Forbidden Dependencies

Use the Dependency Guard or forbiddenDependencies extension to enforce rules:

// custom Gradle check
tasks.register("checkModuleDependencies") {
    doLast {
        // Ensure :feature modules don't depend on each other
        val featureModules = subprojects.filter { it.name.startsWith("feature") }
        featureModules.forEach { module ->
            module.configurations.forEach { config ->
                config.dependencies.forEach { dep ->
                    check(dep !is ProjectDependency ||
                        !dep.name.startsWith("feature")) {
                        "Module ${module.name} cannot depend on ${dep.name}"
                    }
                }
            }
        }
    }
}

Gradle Build Cache and Parallel Builds

A good dependency graph unlocks Gradle parallelism:

// gradle.properties
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.caching=true

Independent modules (those not in each other's dependency chain) build in parallel. A flat, well-separated graph maximizes this.

Key Takeaways

ConceptRule
DAG requirementNo cycles; enforce with CI
internal keywordHide implementation; expose only contracts
API modulesSeparate interface (:api) from implementation (:impl)
Features ↔ FeaturesNever direct; communicate via navigation or shared domain
Build cacheOnly works if module inputs don't change; keep modules focused

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Dependency Graph & API Contracts | Android System Design | Android Engineers