Clean Architecture for Small Android Teams: One Module, No Over-Engineering
Multi-module setups add Gradle complexity, slow down debugging, and scatter related code across modules for teams that rarely hit the collaboration or compile-time problems modules solve. A single-module Clean Architecture with feature packages delivers the same dependency discipline without paying the module tax before it's needed.
A single-module Clean Architecture approach keeps feature code together inside packages, avoiding the cross-module navigation and build-config overhead that hurts small teams. The structure organizes code by feature, with each feature containing its own presentation, domain, and data layers. Dependency inversion is enforced through package boundaries rather than module boundaries, keeping domain logic independent of frameworks like Retrofit or Room. The approach also avoids premature abstraction: UseCases only exist when there is real business logic to orchestrate, and the data layer stays thin unless multiple data sources genuinely require it. Koin is recommended over heavier DI frameworks to keep tooling complexity low. The package structure is designed to evolve into separate Gradle modules later if the project grows, making it a low-cost starting point that doesn't block future scaling.
Many Android teams adopt multi-module architecture because it's presented as a best practice, but the original motivation is large-team governance, not code quality. Applying it to a 2-person project solves problems the team doesn't have.
The advice to put Repository interfaces in the domain layer and implementations in data is a concrete, testable rule that prevents the most common Clean Architecture mistake: domain depending on frameworks.
Recommending Koin over Hilt for small teams is a pragmatic call that prioritizes build speed and simplicity over compile-time safety, which matters less when the codebase is small and the team knows it well.
The argument that UseCases should only exist when they orchestrate business logic pushes back against the cargo-cult pattern of creating one UseCase per repository method, which bloats code without adding value.
Qiangqiang, one more thing — resources can also be divided by module. That makes migrating to multiple modules later much easier. You could add a Gradle task to enforce that resource names under a feature must start with the feature name. Something like this sourceSets { main { file('src/main/res-module') .listFiles() .each { res.srcDirs += it.path } }
Haha, awesome!