====== Decisiones arquitectónicas del proyecto PAE ======
===== ADR: Architecture Decision Records =====
Documento que registra las decisiones de arquitectura clave y su justificación.
----
===== ADR-001: Arquitectura Multicapa (Clean Architecture) =====
==== Fecha: Inicial ====
==== Decisión ====
El proyecto PAE utiliza **Clean Architecture** con separación clara entre:
* **Presentación** (Composable UI)
* **Dominio** (Business logic)
* **Datos** (Persistencia y APIs)
==== Contexto ====
* Proyecto grande con dos aplicaciones Android independientes
* Necesidad de reutilizar código entre Machine y RutaPAE
* Facilitar testing y mantenimiento a largo plazo
==== Alternativas consideradas ====
- **MVP/MVVM simple**: menos testeable, acoplamiento UI-DB
- **Arquitectura monolítica**: difícil de escalar
- **Arquitectura hexagonal**: más compleja, innecesaria para este scope
==== Consecuencias ====
✅ **Positivas**:
* Testabilidad: cada capa se prueba independientemente
* Reusabilidad: módulos pueden reutilizarse en otros proyectos
* Mantenibilidad: cambios aislados a cada capa
* Escalabilidad: fácil agregar nuevas funcionalidades
❌ **Negativas**:
* Más archivos y carpetas
* Requiere mayor disciplina en separación de responsabilidades
* Curva de aprendizaje para nuevos desarrolladores
----
===== ADR-002: Máquina de Estados para Entregas =====
==== Decisión ====
El flujo de captura de entrega en Machine se implementa como una **máquina de estados** lineal con 8 estados predefinidos.
WaitingForWeight → CaptureImages → CaptureFace → ComparingWeights →
GenerateEmbedding → VerifyInDatabase → SaveDelivery → WaitForWeightRemoved
==== Contexto ====
* Proceso de entrega tiene múltiples pasos ordenados
* Cada paso puede fallar independientemente
* Necesidad de reintentos y recuperación de errores
* UI necesita mostrar progreso y estado actual
==== Alternativas consideradas ====
- **Procedimientos lineales**: sin recuperación de errores
- **Callbacks anidados**: "callback hell", difícil mantener
- **Eventos (event bus)**: desorden de eventos async
- **Corrutinas simples**: sin persistencia de estado
==== Consecuencias ====
✅ **Positivas**:
* Flujo predecible y serializable
* Recuperación de errores clara (retry)
* Fácil seguimiento del progreso
* Estado persistible entre sesiones
❌ **Negativas**:
* Agregar nuevos estados requiere cambio en muchos lugares
* Estados deben ser completamente independientes
* Difícil cambiar orden después de deployment
----
===== ADR-003: Repository Pattern para acceso a datos =====
==== Decisión ====
Se implementa el **Repository Pattern** en ''MachineData'' y ''RutaPAEData'' para abstraer acceso a persistencia.
interface Repository {
val deliveries: Table
val beneficiaries: Table
fun create(entity: Entity): Entity
fun update(entity: Entity): Boolean
fun delete(entity: Entity): Boolean
}
==== Contexto ====
* Base de datos SQLite con índices vectoriales (VectorDB)
* Múltiples tipos de datos con operaciones de lectura/escritura
* Posible migración futura a base de datos diferente
* Necesidad de testing sin BD real
==== Alternativas consideradas ====
- **Acceso directo a BD**: acoplamiento fuerte
- **ORM genérico**: framework heavy (Room sería más pesado)
- **DAO simple**: menos abstracción
- **SQLite directo**: sin abstracción
==== Consecuencias ====
✅ **Positivas**:
* Fácil cambiar implementación de BD
* Mock para testing
* API consistente para todas las entidades
* Independencia de framework
❌ **Negativas**:
* Más código de boilerplate
* Mapeo manual entre DTOs y entidades
* Overhead de abstracción
----
===== ADR-004: P2P con DirectLink (Wi-Fi Direct) =====
==== Decisión ====
La comunicación entre Machine y RutaPAE usa **DirectLink** (Wi-Fi Direct/hotspot) como transporte primario.
==== Contexto ====
* Máquinas en zonas rurales sin conectividad consistente
* Necesidad de sincronización sin depender de internet
* RutaPAE debe descubrir máquinas automáticamente
* Backup a conexión HTTP si P2P no disponible
==== Alternativas consideradas ====
- **Solo HTTP**: depende de conectividad
- **Bluetooth**: rango limitado
- **Mesh networks**: complejidad innecesaria
- **Firebase/Cloud Messaging**: requiere internet
- **Bluetooth + HTTP**: peor que P2P + HTTP
==== Consecuencias ====
✅ **Positivas**:
* Funciona sin internet
* Local, sin latencia de cloud
* Rápido para datos grandes (fotos)
* Seguro: sin tráfico por servidor externo
❌ **Negativas**:
* Requiere permisos adicionales en Android
* No funciona con todas las mezclas de dispositivos
* Implica manejo de hotspot en Machine
----
===== ADR-005: VectorDB con embeddings 512d =====
==== Decisión ====
Se implementa **VectorDB ORM** con índices vectoriales para almacenar embeddings de beneficiarios.
@VectorColumn(dimensions = 512, distanceMetric = DistanceMetric.COSINE)
val embedding: FloatArray
==== Contexto ====
* Necesidad de búsqueda por similitud facial
* Operaciones locales sin enviar fotos al servidor
* Requisitos de privacidad: fotos nunca salen del dispositivo
* Precisión importante para matching correcto
==== Alternativas consideradas ====
- **Solo búsqueda por nombre**: impreciso
- **Fotos y búsqueda remota**: viola privacidad
- **Hashing de imágenes**: pérdida de información
- **Face API remota**: depende de conectividad
- **Almacenar embeddings sin índice**: búsqueda O(n)
==== Consecuencias ====
✅ **Positivas**:
* Privacidad: datos nunca salen del dispositivo
* Rapidez: búsqueda vectorial local
* Precisión: embeddings mantiened información suficiente
* Offline-first: funciona sin conectividad
❌ **Negativas**:
* Requiere framework adicional (VectorDB)
* Consumo de memoria: 512 floats × N beneficiarios
* Aprendizaje de embeddings centralizado (necesita backend)
----
===== ADR-006: Corrutinas de Kotlin para concurrencia =====
==== Decisión ====
Se usa **Kotlin Coroutines** para todas las operaciones asíncronas.
suspend fun syncDeliveriesFromMachine() { ... }
LaunchedEffect {
StateNameEmitter.collect { state -> ... }
}
==== Contexto ====
* Proyecto 100% Kotlin
* UI Composable requiere suspensión
* Operaciones I/O: HTTP, BD, P2P
* Necesidad de cancelación limpia
==== Alternativas consideradas ====
- **Callbacks**: callback hell
- **RxJava/RxKotlin**: framework pesado
- **Threads**: manual tedioso, error-prone
- **AsyncTask**: deprecated
- **Futures/CompletableFuture**: verboso
==== Consecuencias ====
✅ **Positivas**:
* Sintaxis limpia y legible
* Integración nativa con Composable
* Manejo de excepciones familiar
* Cancelación automática
* Bajo overhead
❌ **Negativas**:
* Curva de aprendizaje (suspend, async, launch)
* Debugging de flow más complejo
* Compilación más lenta
----
===== ADR-007: Emisores para comunicación inter-módulos =====
==== Decisión ====
Se usa patrón **Observer con Flow/Emitters** para comunicación entre módulos.
StateNameEmitter.emit("CapturingFace")
StateNameEmitter.collect { name -> ... }
==== Contexto ====
* Comunicación desacoplada entre Dominio y Presentación
* Múltiples observadores posibles
* Reactividad necesaria
* Necesidad de serialización mínima
==== Alternativas consideradas ====
- **Callbacks directos**: acoplamiento fuerte
- **Live Data**: deprecado, Framework Android
- **Event Bus (EventBus/Otto)**: reflexión, overhead
- **StateFlow global**: estado mutable shared
- **Intent (broadcast)**: para cross-process, innecesario aquí
==== Consecuencias ====
✅ **Positivas**:
* Desacoplamiento: productor no conoce consumidores
* Múltiples observadores posibles
* Type-safe
* Cancelación automática con scope
* Performante
❌ **Negativas**:
* Debugging: flujo de datos menos visible
* Posibles memory leaks si no se cancela
* Test debe mockear Flow
----
===== ADR-008: Inyección de dependencias diferenciada =====
==== Decisión ====
* Machine usa **Dagger2/Hilt**
* RutaPAE usa **inyección manual**
* MachineDomain/RutaPAEDomain usan **inyección por constructor**
==== Contexto ====
* Machine: app compleja con muchos componentes
* RutaPAE: app más simple, control centralizado en DomainManager
* Módulos de lógica: necesitan flexibilidad en testing
==== Alternativas consideradas ====
- **Hilt en todas partes**: overhead para RutaPAE
- **Service Locator**: anti-pattern
- **Singleton global**: testing difícil
- **Factory methods**: boilerplate
==== Consecuencias ====
✅ **Positivas**:
* Apropiado para cada caso (complejidad vs simplicidad)
* Control fino en RutaPAE
* Automatización en Machine
* Testeable
❌ **Negativas**:
* Inconsistencia entre apps
* Desarrolladores aprenden dos patrones
----
===== ADR-009: Modelos P2P en Contract separado =====
==== Decisión ====
Se crea módulo ''Contract'' con modelos P2P compartidos entre Machine y RutaPAE.
P2PMachineState
P2PDeliveryData
P2PMachineConfiguration
// ...
==== Contexto ====
* Necesidad de versioning de API P2P
* Ambas apps deben conocer el mismo formato
* Cambios coordin
ados entre apps
==== Alternativas consideradas ====
- **Modelos en Machine, RutaPAE importa**: acoplamiento
- **Modelos duplicados**: desincronización futura
- **Serialización ad-hoc**: error-prone
- **GraphQL schema**: overhead innecesario
==== Consecuencias ====
✅ **Positivas**:
* Single source of truth
* Ambas apps sincronizadas
* Versionable
* Testing de serialización centralizado
❌ **Negativas**:
* Módulo adicional
* Cambios coordinados entre apps
----
===== ADR-010: WorkManager para sync en background =====
==== Decisión ====
Se usa **WorkManager** en RutaPAE para sincronización de entregas en background.
DeliverySyncWorker : CoroutineWorker {
override suspend fun doWork(): Result
}
DeliverySyncScheduler.enqueue(context, machineId)
==== Contexto ====
* Sincronización debe ocurrir incluso con app cerrada
* Límites de background execution en Android 8+
* Necesidad de reintentos automáticos
* Batching de requests
==== Alternativas consideradas ====
- **Foreground Service**: requiere notificación
- **Handler/Timer**: impreciso
- **Alarm Manager**: timing inflexible
- **Firebase Job Dispatcher**: obsoleto
- **Sync Adapter**: para providers
==== Consecuencias ====
✅ **Positivas**:
* Respeta límites de Android
* Reintentos automáticos
* Batching eficiente
* Diagnóstico integrado
❌ **Negativas**:
* Ejecución no está garantizada (timing)
* Debugging más complejo
* Requiere WorkManager dependency
----
===== ADR-011: UI con Jetpack Compose =====
==== Decisión ====
Ambas apps usan **Jetpack Compose** exclusivamente para UI.
@Composable
fun LaboratoryScreen(viewModel: LaboratoryViewModel) { ... }
==== Contexto ====
* Proyecto nuevo: sin legacy XML layouts
* Mejor productividad
* Hot reload en desarrollo
* UI declarativa más mantenible
==== Alternativas consideradas ====
- **XML layouts**: más verbose
- **Flutter**: cambio de stack
- **SwiftUI**: solo iOS
- **Hybrid (Compose+XML)**: inconsistencia
==== Consecuencias ====
✅ **Positivas**:
* Sintaxis limpia
* Hot reload
* Menos boilerplate
* Preview en IDE
❌ **Negativas**:
* Composability learning curve
* Compilación más lenta
* Debugging de recomposition complejo
----
===== ADR-012: Dos aplicaciones separadas (Machine + RutaPAE) =====
==== Decisión ====
Se mantienen **dos aplicaciones Android separadas** con sus propios builds y releases.
==== Contexto ====
* Diferentes funcionalidades
* Diferentes workflows
* Diferentes versiones
* Diferentes firmas de certificado
==== Alternativas consideradas ====
- **Una app con modos**: complejidad, conflicto de UX
- **Shared codebase con build variants**: difícil mantener
- **PWA + Android**: scope diferente
==== Consecuencias ====
✅ **Positivas**:
* Liberación independiente
* Claridad de propósito
* Sin complejidad condicional
❌ **Negativas**:
* Duplicación de código base
* Cambios deben aplicarse a ambas
----
===== ADR-013: SQLite + VectorDB ORM =====
==== Decisión ====
Se usa **SQLite con VectorDB ORM** en lugar de Room o Firebase.
==== Contexto ====
* Necesidad de índices vectoriales
* Control fino de esquema
* Sin dependencias grandes
* Offline-first
==== Alternativas consideradas ====
- **Room**: no tiene soporte vectorial
- **Firebase**: requiere conectividad
- **Realm**: propietario
- **Raw SQLite**: sin ORM
- **PostgreSQL embeddo**: demasiado pesado
==== Consecuencias ====
✅ **Positivas**:
* Índices vectoriales nativos
* Control completo
* Ligero
* Offline
❌ **Negativas**:
* API customizada
* Menos tooling
* Migraciones manuales
----
===== Principios guía =====
Estos son los principios que guían las decisiones arquitectónicas en PAE:
- **Offline-first**: el sistema funciona sin conectividad
- **Privacy-first**: datos sensibles nunca salen del dispositivo
- **Responsabilidad única**: cada módulo hace una cosa bien
- **Inversión de dependencias**: abstractas, no concretas
- **Open/Closed**: abierto para extensión, cerrado para modificación
- **DRY (Don't Repeat Yourself)**: evitar duplicación
- **KISS (Keep It Simple, Stupid)**: no over-engineer
- **Testeable**: todo debe testearse fácilmente
- **Performance**: observar impacto en batería y memoria
- **User experience**: la arquitectura debe servir al usuario, no al revés
----
===== Futuras decisiones pendientes =====
* Migración a Kotlin Multiplatform (iOS, Backend)
* Agregación de analytics (privado, local)
* Expansión a más modalidades de entrega
* Integración con más proveedores de verificación biométrica