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:
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:
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<Delivery>
val beneficiaries: Table<Beneficiary>
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:
❌ Negativas:
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:
ADR-008: Inyección de dependencias diferenciada
Decisión
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:
❌ Negativas:
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
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:
❌ Negativas:
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:
❌ Negativas:
ADR-011: UI con Jetpack Compose
Decisión
Ambas apps usan Jetpack Compose exclusivamente para UI.
@Composable
fun LaboratoryScreen(viewModel: LaboratoryViewModel) { ... }
Contexto
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:
ADR-012: Dos aplicaciones separadas (Machine + RutaPAE)
Decisión
Se mantienen dos aplicaciones Android separadas con sus propios builds y releases.
Contexto
Alternativas consideradas
Una app con modos: complejidad, conflicto de UX
Shared codebase con build variants: difícil mantener
PWA + Android: scope diferente
Consecuencias
✅ Positivas:
❌ Negativas:
ADR-013: SQLite + VectorDB ORM
Decisión
Se usa SQLite con VectorDB ORM en lugar de Room o Firebase.
Contexto
Alternativas consideradas
Room: no tiene soporte vectorial
Firebase: requiere conectividad
Realm: propietario
Raw SQLite: sin ORM
PostgreSQL embeddo: demasiado pesado
Consecuencias
✅ Positivas:
❌ Negativas:
-
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