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