(04) Sistemas / Caso de estudio
yourFinance
Los saldos los garantiza la base de datos.
yourFinance es una app de finanzas personales multiusuario: una API en Rust, PostgreSQL y un solo cliente para iOS, Android, web y escritorio. El código es privado; esta página explica cómo está hecha y por qué.
- Inicio
- Julio de 2025
- Versión
- 1.3.0, octubre de 2026
- Equipo
- Dos desarrolladores
- Código
- Privado
Rust · Axum · SQLx · PostgreSQL 18 · Expo · React Native · Tauri · Docker
El problema
Los datos de finanzas personales solo sirven si uno se puede fiar de los saldos. Varias personas usan la app desde varios dispositivos, y un error en un cliente o en un handler no puede dejar una cuenta con una cifra equivocada.
Además tiene que manejar más de una divisa, importar el historial completo de otra app (Money Manager) y mostrar estadísticas que nunca estén desfasadas.
Restricciones
- La API nunca escribe un saldo: cada cambio es un movimiento, también un ajuste de saldo.
- Cada fila personal pertenece a un usuario, y PostgreSQL repite la regla con seguridad a nivel de fila en cada conexión del pool.
- Un solo código para iOS, Android y web; la app de escritorio es la versión web dentro de Tauri.
- Un cambio en el esquema o en la API tiene que romper la compilación, no la app en uso: el SQL se comprueba al compilar y los tipos del cliente se generan a partir del documento OpenAPI.
Arquitectura
Tres niveles: el cliente solo muestra, la API autentica, valida y agrega, y la base de datos es la fuente de verdad. La API es un workspace de Cargo con crates por capas: cada uno depende solo de los que están por debajo, y la CI comprueba que el dominio no haga E/S.
yourf-api
HTTP: rutas finas, autenticación, traducción de errores, OpenAPI.
yourf-worker
Tareas en segundo plano: notificaciones push, resumen de alertas, movimientos recurrentes, conciliación de saldos.
yourf-runtime
Lo que comparten los dos binarios: configuración, telemetría, ciclo de vida.
yourf-db
Repositorios SQLx, ámbito de la seguridad a nivel de fila, escrituras en una sola transacción.
yourf-import
Lectores de archivos de otras apps a un formato neutro, sin base de datos.
yourf-domain
Modelos, contratos y reglas de negocio. Sin E/S.
Dos binarios, una imagen: API y worker se despliegan y escalan por separado.
Una petición, de principio a fin
Crear un movimiento:
- 01Cliente
El formulario valida el borrador y lo envía con el token.
- 02API
Autentica el token, carga el usuario, valida el cuerpo e inserta el movimiento con sus repartos por cuenta y categoría en una sola transacción.
- 03Base de datos
Unos triggers diferidos comprueban que los repartos cuadren y que cuentas, categorías y etiquetas sean del usuario, y después actualizan los saldos. Si se incumple una regla se deshace todo y la respuesta es un 400 con un código de regla estable.
- 04Cliente
Si todo va bien, se recargan las listas, los totales y el panel.
Decisiones y compromisos
- 01
La integridad está en la base de datos
Saldos, propiedad de los datos y reglas de las categorías se imponen con triggers. Supone escribir lógica en SQL en lugar de Rust; a cambio, ningún cliente ni ningún handler puede corromper los datos.
- 02
Saldos comprobables, no una contabilidad de partida doble completa
Cada efecto sobre un saldo es un apunte con signo y una única definición, que usan tanto el trigger que escribe los saldos como la tarea que los concilia. Un libro mayor completo solo compensaría la reescritura si alguna función necesitara un balance.
- 03
Una transferencia es un movimiento con dos importes
Lo que salió de la cuenta de origen y lo que llegó a la de destino. La diferencia cubre tanto un cambio de divisa como una comisión, que por eso no aparece como gasto en las estadísticas.
- 04
Sin cola de trabajos
El worker toma las filas pendientes con FOR UPDATE SKIP LOCKED y el outbox de notificaciones se despierta con LISTEN/NOTIFY. Cada pasada es idempotente: ejecutarla dos veces, o en varias réplicas, da el mismo resultado.
- 05
Estadísticas calculadas en cada petición
El panel y las estadísticas agregan los movimientos en SQL cada vez: sin agregados precalculados ni caché, así que las cifras nunca están desfasadas. El precio es que las consultas tienen que ser rápidas por sí mismas.
Medido
Al perfilar las consultas de estadísticas vi que dos crecían con todo el historial en lugar de con el periodo elegido. Con el plan genérico de una sentencia preparada, el planificador elegía un nested loop que repetía un join una vez por movimiento. Los índices no eran el problema: la solución fue reescribir la vista para que cada rama empiece por el movimiento.
| Consulta | Antes | Después |
|---|---|---|
| Flujos diarios (tendencia de las estadísticas)daily_flows | 64.5 ms | 1.9 ms |
| Gastos esenciales (resumen del panel)essential_expenses | 48.0 ms | 1.5 ms |
La app



Lo que viene
- Fecha de vencimiento en las cuentas, con un aviso antes de que llegue.
- Amortización: un gasto repartido en varios meses en las estadísticas, mientras el saldo sigue reflejando la caja.
- Un asistente que responde preguntas sobre estadísticas y cuentas con herramientas de solo lectura: el modelo elige la herramienta y el backend calcula cada número.
Código y demo
El repositorio es privado. Si me lo pides, te enseño el código y la app en funcionamiento.