Saltar al contenido
PG
Todos los proyectos

FinCore

Cuentas, movimientos, transferencias y presupuestos sobre una API Spring Boot con dinero en BigDecimal e idempotencia de extremo a extremo.

En 30 segundos

Categoría
Full stack · Finanzas personales
Estado
API completa · la web cubre el uso diario · sin demo pública
Stack
Java 21 · Spring Boot 3 · PostgreSQL 16 · Flyway · React 19
Tests
150 de integración con Testcontainers + 72 métodos unitarios + 15 del frontend
Arquitectura
Monolito modular · 16 módulos · eventos de Spring entre módulos
Alcance de la web
Registro, resumen, cuentas, movimientos, transferencias, presupuestos y categorías. El resto, desde la API (Swagger).
Demo online
Sin demo pública

Visión general

Qué es
Una aplicación de finanzas personales con una API REST muy completa (cuentas, movimientos, transferencias, presupuestos, objetivos, suscripciones, facturas, importación CSV, multimoneda y analítica) y una web en React para el uso diario.
Qué problema resuelve
Llevar las cuentas propias exige que los saldos sean exactos y que nada se registre dos veces, aunque falle la red o se pulse dos veces el mismo botón.
Para quién
Una persona que quiere controlar sus cuentas, su gasto por categorías y sus presupuestos.
Objetivo
Que la lógica financiera sea correcta por diseño: dinero exacto, operaciones atómicas, peticiones repetidas sin efecto y un historial que no se reescribe.

Vista previa

fincore · vista previa
Datos de demostración. Resumen de FinCore: patrimonio, ingresos y gastos del mes, últimos movimientos y presupuestos en curso.
Datos de demostración. El resumen de FinCore a 390 px de ancho.

Funcionalidades

Implementado

Cuentas y movimientos

Cuentas de banco, tarjeta, efectivo o ahorro con su moneda y estados (activa, archivada, cerrada) con transiciones permitidas. Ingresos y gastos que actualizan el saldo, con búsqueda, filtros y ordenación.

Para qué sirve: Los filtros se guardan en la URL: al recargar o compartir el enlace se ve la misma lista.

Datos de demostración. Lista de movimientos con filtros por cuenta, categoría, fechas y tipo.
Implementado

Transferencias entre cuentas

Una transferencia crea las dos patas (salida y entrada) y actualiza los dos saldos en una sola transacción.

Para qué sirve: Nunca queda una cuenta descontada y la otra sin abonar.

Datos de demostración. Formulario de transferencia entre cuentas con origen, destino, importe, fecha y concepto.
Implementado

Corregir sin reescribir el historial

El backend no edita movimientos, solo los anula. Corregir uno desde la web crea la versión buena como movimiento nuevo y anula el original, que sigue visible como anulado.

Para qué sirve: Siempre se puede ver qué cambió y cuándo.

Recurso pendienteCaptura pendiente: movimiento corregido y original anulado

Implementado

Presupuestos con avisos

Presupuestos por categoría y periodo que avisan al llegar al 80 % y al 100 %. El aviso se genera con un evento que recoge el módulo de notificaciones.

Para qué sirve: Saber que te acercas al límite antes de pasarlo.

Datos de demostración. Presupuestos mensuales por categoría con el gasto actual, el límite y el porcentaje usado.
Parcial

Categorías y reglas automáticas

Categorías del sistema y propias, con subcategorías. La API tiene además reglas que categorizan los movimientos automáticamente; en la web solo están las categorías.

Para qué sirve: El gasto se agrupa sin clasificar cada movimiento a mano.

Datos de demostración. Listado de categorías del sistema y propias.
Parcial

Funciones solo disponibles en la API

Objetivos de ahorro, suscripciones y facturas, importación de extractos CSV con detección de duplicados, multimoneda con tipos de cambio históricos y analítica detallada. Están implementadas y probadas en el backend, pero la web todavía no las muestra.

Para qué sirve: La API está documentada en Swagger y se puede usar completa desde ahí.

Datos de demostración. API de FinCore en Swagger UI, con los grupos de endpoints de cuentas, movimientos, presupuestos y analítica.

Stack

Backend

Java 21 + Spring Boot 3.3
API REST (Web, Data JPA, Validation, Actuator)
Spring Security + jjwt + BCrypt
Autenticación y sesiones
springdoc-openapi
Documentación Swagger UI
Apache Commons CSV
Importación de extractos
Micrometer + Prometheus
Métricas de negocio y de la aplicación

Base de datos

PostgreSQL 16
Datos y agregaciones de la analítica en SQL
Flyway
23 migraciones versionadas; Hibernate solo valida

Frontend

React 19 + Vite + TypeScript
Web para el uso diario
TanStack Query + React Router
Datos del servidor y navegación
React Hook Form + Zod
Formularios
Tailwind CSS 4
Estilos; componentes propios con <dialog> nativo

Testing

JUnit 5 + Spring Boot Test
Tests unitarios (Surefire) y de integración (Failsafe)
Testcontainers
PostgreSQL real en los tests de integración
Vitest
Lógica del frontend

Infraestructura

Docker + Docker Compose
PostgreSQL, API y web
nginx
Sirve la web y reenvía /api: un solo origen, sin CORS
GitHub Actions
mvnw verify, lint, tests, build e imágenes Docker

Arquitectura

Un monolito modular: un solo proyecto organizado por áreas de negocio. Cada módulo se divide en dominio, aplicación e infraestructura, y los efectos secundarios viajan como eventos.

Arquitectura de FinCoreLa web React llama a nginx, que reenvía /api a la aplicación Spring Boot. La aplicación es un monolito modular: cada módulo tiene capas de dominio, aplicación e infraestructura, y los módulos se avisan con eventos. Los datos están en PostgreSQL con migraciones Flyway.ClienteWeb React 19TanStack Query · ZodHTTPEdgenginxun solo origen/api/v1Spring Boot 3Monolito modular16 módulos · eventosinfrastructure/webapplicationdomainSpring SecurityJWT · rate limitJPA · SQLDatosPostgreSQL 16bloqueos pesimistasFlyway23 migraciones
  • domain contiene entidades y reglas que no dependen de Spring (por ejemplo, las transiciones de estado de una cuenta o de una factura).
  • application contiene los casos de uso, los eventos y las excepciones de negocio; infrastructure, los controladores REST, los DTOs y los repositorios.
  • Los módulos se avisan con eventos de Spring: presupuestos publica que se superó un umbral y notificaciones lo recoge sin que presupuestos sepa que existe.
  • La web habla con un solo origen: Vite en desarrollo y nginx en Docker reenvían /api al backend.

Decisiones técnicas

Cada decisión con el problema, la solución, el motivo y el lugar exacto del código donde está.

Dinero con BigDecimal y redondeo bancario

Problema
Con double, 0,1 + 0,2 no da 0,3, y sumar importes de monedas distintas da un número sin sentido.
Solución
Un tipo Money que guarda importe y moneda, ajusta la escala a los decimales de cada moneda con HALF_EVEN y lanza una excepción si se opera con monedas distintas.
Por qué
El error se detecta al escribir el código, no en un saldo que no cuadra meses después. El redondeo bancario evita el sesgo hacia arriba al redondear muchos importes.
Código
Test

“rejectsOperationsAcrossDifferentCurrencies”

MoneyTest.java:33 (se abre en una pestaña nueva)

Transferencias con bloqueo ordenado

Problema
Dos transferencias cruzadas (A→B y B→A) a la vez pueden bloquearse mutuamente, y sin bloqueo dos operaciones pueden pisar el saldo de la otra.
Solución
La transferencia es una sola transacción que bloquea las dos cuentas con PESSIMISTIC_WRITE, siempre ordenadas por su UUID, antes de tocar los saldos.
Por qué
Bloquear en el mismo orden impide el ciclo de espera, y el bloqueo pesimista garantiza que nadie lee un saldo que está a punto de cambiar.
Código
Test

“crossedConcurrentTransfersDoNotDeadlockAndKeepTotals”

TransferControllerIT.java:112 (se abre en una pestaña nueva)

Idempotencia de extremo a extremo

Problema
Con mala cobertura, el cliente puede reenviar la misma petición y registrar el mismo gasto o la misma transferencia dos veces.
Solución
La cabecera Idempotency-Key: la segunda petición con la misma clave devuelve la respuesta de la primera; con la misma clave y datos distintos se rechaza. La web genera la clave al abrir cada formulario.
Por qué
Reintentar se vuelve seguro, también en la corrección de movimientos: si la anulación falla, se puede repetir sin duplicar nada.
Código
Test

“concurrentTransfersWithTheSameKeyExecuteOnlyOnce”

IdempotencyIT.java:147 (se abre en una pestaña nueva)

El historial no se edita

Problema
Si un movimiento se puede modificar, el saldo de ayer deja de poder explicarse.
Solución
El backend solo permite anular y reembolsar. Anular una pata de una transferencia anula las dos, y los reembolsos nunca superan el gasto original.
Por qué
Es como funciona la contabilidad real: se corrige añadiendo, no borrando.
Código
Test

“concurrentRefundsNeverExceedTheExpense”

VoidAndRefundIT.java:117 (se abre en una pestaña nueva)

Módulos desacoplados con eventos

Problema
Si presupuestos llamara directamente a notificaciones, cada módulo nuevo que quisiera reaccionar obligaría a tocar presupuestos.
Solución
Presupuestos publica BudgetThresholdReachedEvent y notificaciones lo escucha con @TransactionalEventListener(AFTER_COMMIT).
Por qué
El aviso solo se crea si la operación se confirmó, y los módulos pueden crecer sin conocerse.
Código

Lo que falta

  • La web no cubre objetivos, suscripciones, facturas, importación CSV, reglas, sesiones ni la analítica detallada: se usan desde la API.
  • Los tokens se guardan en localStorage; pasar a cookies httpOnly cambiaría el contrato de la API.
  • Los emails se escriben en el log; para producción haría falta un servidor SMTP.
  • El límite de peticiones se guarda en memoria: con varias instancias, cada una llevaría su cuenta.