La estructura canónica
No hay una única estructura válida, pero la que escala mejor en proyectos Android grandes tiene esta forma:
miapp/
├── app/ # :app — punto de entrada, poco código
├── feature/
│ ├── productos/ # :feature:productos
│ ├── checkout/ # :feature:checkout
│ ├── auth/ # :feature:auth
│ └── perfil/ # :feature:perfil
├── core/
│ ├── ui/ # :core:ui — componentes visuales compartidos
│ ├── network/ # :core:network — OkHttp, Retrofit base
│ ├── database/ # :core:database — Room base, AppDatabase
│ ├── domain/ # :core:domain — modelos e interfaces de repo
│ ├── data/ # :core:data — implementaciones de repo
│ └── testing/ # :core:testing — Fakes compartidos
└── build-logic/ # convention plugins (no es un módulo de app)
:app — el módulo que no hace nada
El módulo :app debería ser lo más delgado posible. Su función es ser el punto de entrada y conectar las piezas — no contener lógica.
// Lo que SÍ vive en :app:
// - Application class (@HiltAndroidApp)
// - MainActivity (el contenedor de navegación)
// - El NavGraph raíz que conecta todas las features
// - Los módulos de Hilt que conectan features que no se conocen entre sí
// Lo que NO debería vivir en :app:
// - Lógica de negocio
// - ViewModels (salvo el de MainActivity)
// - DAOs o repositorios
// - Pantallas de features específicas
// settings.gradle — declarar todos los módulos
include(":app")
include(":feature:productos")
include(":feature:checkout")
include(":core:ui")
include(":core:network")
include(":core:database")
include(":core:domain")
// :app/build.gradle — depende de todo, pero no expone nada
dependencies {
implementation(project(":feature:productos"))
implementation(project(":feature:checkout"))
implementation(project(":feature:auth"))
implementation(project(":core:network"))
implementation(project(":core:database"))
}
:feature:* — una pantalla o flujo de usuario
Un módulo de feature encapsula todo lo necesario para un flujo completo: UI, ViewModel, y si corresponde, su propia capa de datos local. Lo que lo define es que podés describirlo en términos de lo que el usuario hace, no de lo que el código hace.
# Ejemplos de buenas fronteras de feature:
# :feature:auth → todo el flujo de login/registro
# :feature:checkout → el flujo de carrito y pago
# :feature:productos → la lista y el detalle de productos
# :feature:perfil → la pantalla de perfil del usuario
# Ejemplos de malas fronteras:
# :feature:dialogs → no es un flujo, es un tipo de componente
# :feature:utils → "utils" no describe ninguna responsabilidad
# :feature:todo → demasiado amplio para ser un módulo útil
# Estructura interna de un feature module:
feature/checkout/
├── src/main/java/ar/pensa/app/feature/checkout/
│ ├── data/
│ │ ├── CheckoutRepository.kt # interfaz (puede ir en :core:domain)
│ │ └── CheckoutRepositoryImpl.kt
│ ├── domain/
│ │ └── ProcesarPagoUseCase.kt
│ ├── presentation/
│ │ ├── CheckoutViewModel.kt
│ │ ├── CheckoutFragment.kt
│ │ └── CheckoutUiState.kt
│ └── di/
│ └── CheckoutModule.kt # módulo Hilt de esta feature
└── build.gradle
Las features no se importan entre síEsta es la regla más importante. Si :feature:checkout necesita datos de :feature:productos, esos datos deben estar en :core:domain y ambas features los consumen desde ahí. Si dos features se importan mutuamente, tenés una dependencia circular y el build falla.
:core:* — lo compartido entre features
Los módulos :core contienen código que múltiples features necesitan. La regla: si solo una feature lo usa, va en esa feature. Si dos o más lo necesitan, va en core.
// :core:network — configuración de red compartida
// Contiene: OkHttp, Retrofit base, interceptors de auth, NetworkMonitor
// NO contiene: las APIs específicas de cada feature (esas van en :feature:*)
// :core:database — Room base
// Contiene: AppDatabase, los DAOs compartidos, migrations
// NO contiene: entities específicas de una feature (pueden ir en :feature:*)
// :core:domain — contratos de negocio
// Contiene: interfaces de repositorio, modelos de dominio, use cases base
// NO depende de Android (Kotlin puro)
// Es el módulo que más features consumen
// :core:ui — componentes visuales compartidos
// Contiene: botones custom, cards, loaders, el tema de la app
// Todos los feature modules lo consumen
// :core:data — implementaciones compartidas de repositorio
// Contiene: implementaciones de las interfaces de :core:domain
// Depende de :core:network y :core:database
Módulos Kotlin puros — sin Android
Algunos módulos no necesitan nada del framework de Android. Aplicar el plugin java-library en lugar de com.android.library los hace más livianos, compilan más rápido, y pueden usarse en proyectos Kotlin puro o KMP:
// :core:domain/build.gradle — módulo Kotlin puro
plugins {
id("java-library")
alias(libs.plugins.kotlin.jvm)
}
// No tiene dependencias de Android — solo Kotlin
dependencies {
implementation(libs.kotlinx.coroutines.core)
}
// Las clases en este módulo no pueden importar nada de android.*
// Esto fuerza que el dominio sea verdaderamente independiente del framework
El grafo de dependencias — la ley de gravedad del proyecto
# El grafo correcto tiene un solo sentido: hacia abajo
#
# :app
# / | \
# :feature:* :feature:*
# \ | /
# :core:ui
# :core:data
# |
# :core:domain ← el más consumido, el más estable
# |
# :core:network
# :core:database
#
# Reglas:
# → :app puede depender de cualquier módulo
# → :feature:* puede depender de :core:* pero NO de otros :feature:*
# → :core:* puede depender de módulos core de nivel más bajo
# → :core:domain NO depende de nada de Android (Kotlin puro)
# → Ningún módulo puede crear un ciclo de dependencias
Las tres reglas que no se rompen
# Regla 1: Las features no se conocen entre sí
# :feature:checkout no importa nada de :feature:productos
# Si necesita datos de productos, los obtiene de :core:domain
# Regla 2: :core:domain no depende de Android
# Ningún import de android.* en los modelos, interfaces o use cases de dominio
# Esto garantiza que el dominio es testeable sin emulador
# Regla 3: :app es el integrador, no el implementador
# :app conecta features, no implementa lógica
# Si algo en :app crece, es señal de que falta un módulo