Sistema bancario simulado con arquitectura de microservicios, comunicación asíncrona basada en eventos (Apache Kafka) y principios de CQRS, Outbox Pattern y Zero Trust Security.
nginx:8888
|
┌──────────────┴──────────────┐
│ api-gateway:8000 │
│ (Spring Cloud Gateway + JWT) │
└──────┬──────┬──────┬──────┬──┘
│ │ │ │
┌───────────┘ │ │ └───────────┐
│ │ │ │
users:8081 accounts:8082 transfers:8083 │
(auth, users, (accounts, (transfers, │
projections) deposits, outbox) │
movements) │
│ │ │ │
└───────────┐ │ │ ┌────────────┘
│ │ │ │
ledger:8084 notifications:8085
(libro contable (notificaciones
+ SSE) SSE en tiempo real)
╔══════════════════════════════════════════════╗
║ Apache Kafka ║
║ bank.transfer.events | bank.account.events ║
║ bank.notification.events | bank.user.events ║
╚══════════════════════════════════════════════╝
╔══════════════════════════════════════════════╗
║ MySQL 8 ║
║ bank_users | bank_accounts | bank_transfers ║
║ bank_ledger | bank_notifications ║
╚══════════════════════════════════════════════╝
| Patrón | Descripción |
|---|---|
| Event-Driven Architecture | Comunicación asíncrona entre servicios mediante Apache Kafka |
| Outbox Pattern | Las órdenes de transferencia se persisten primero en una tabla outbox y luego se publican a Kafka mediante un @Scheduled poller, garantizando consistencia |
| CQRS (ligero) | Lado de escritura en accounts-service + transfers-service; lado de lectura en ledger-service (libro contable con SSE) |
| Database per Service | Cada microservicio tiene su propia base de datos MySQL |
| Zero Trust Security | Validación JWT tanto en el API Gateway como en cada microservicio de forma individual |
| Hexagonal Architecture | Los servicios siguen clean architecture con puertos/adaptadores y paquetes organizados por dominio |
| Idempotency | Todos los POST de transferencias requieren Idempotency-Key; consumers de Kafka ignoran eventos duplicados |
Registro, autenticación y administración de usuarios.
POST /api/auth/register— Registro de nuevo usuarioPOST /api/auth/register-admin— Registro de administrador (requiere bootstrap secret)POST /api/auth/login— Inicio de sesión, devuelve JWT (access + refresh)POST /api/auth/refresh— Renovación del access tokenGET /api/users/me— Perfil propioPUT /api/users/me— Actualizar perfil propioGET /api/admin/users— Listar todos los usuarios (admin)PUT /api/admin/users/{id}/status— Activar/desactivar usuario (admin)PUT /api/admin/users/{id}/role— Cambiar rol de usuario (admin)
Publica UserCreatedEvent en bank.user.events al registrar usuarios.
Gestión de cuentas bancarias, tarjetas, depósitos y movimientos.
POST /api/accounts/create— Crear cuenta bancaria (requierecardIdpara vincular a una tarjeta)GET /api/accounts/me— Listar cuentas del usuario autenticadoGET /api/accounts/{id}— Detalle de una cuentaGET /api/accounts/{id}/movements— Movimientos de una cuentaPATCH /api/accounts/{id}/block— Bloquear/desbloquear cuenta
POST /api/cards/issue— Emitir tarjeta (requierepin4de 4 dígitos +pin6de 6 dígitos)GET /api/cards/me— Listar tarjetas del usuarioGET /api/cards/{id}— Detalle de tarjeta con cuentas vinculadasPUT /api/cards/{id}/pin— Cambiar PIN de tarjetaPATCH /api/cards/{id}/block— Bloquear tarjetaPOST /api/cards/{id}/accounts— Vincular cuenta existente a una tarjeta
POST /api/accounts/deposit— Auto-depósito (directo, sin tarjeta)POST /api/cards/{cardId}/deposit— Depósito con tarjeta (requierepin4,pin6,accountNumbervinculado)
Validaciones en depósito con tarjeta:
- Tarjeta existe y pertenece al usuario
- Tarjeta está
ACTIVE - Tarjeta no está expirada
pin4correctopin6correcto- Cuenta destino existe y está
ACTIVE - Cuenta está vinculada a la tarjeta
Escucha bank.transfer.events para procesar transferencias entrantes (valida saldo, debita origen, acredita destino) y publica resultados en bank.account.events (AccountDebitedEvent, AccountCreditedEvent, AccountRejectedEvent).
Orquestación de transferencias P2P con Outbox Pattern.
POST /api/transfers/internal— Transferencia entre cuentas propias (requiereIdempotency-Key)POST /api/transfers/external— Transferencia a cuenta de tercero (requiereIdempotency-Key)POST /api/transfers/card-payment— Pago con tarjeta (requiereIdempotency-Key,pin4,cardId)GET /api/transfers/{transferId}— Estado de una transferenciaGET /api/transfers/by-account/{accountNumber}— Transferencias por cuenta
Publica TransferRequestedEvent en bank.transfer.events usando el patrón Outbox: persiste el evento en tabla transfer_events y un @Scheduled poller lo envía a Kafka cada 5 segundos, marcándolo como SENT. Consume bank.account.events para actualizar el estado de la transferencia (PENDING → DEBITED → COMPLETED o REJECTED).
Libro contable de doble entrada con SSE.
GET /api/ledger— Todos los asientos contablesGET /api/ledger/{id}— Asiento por IDGET /api/ledger/by-account?accountNumber=— Asientos de una cuentaGET /api/ledger/by-transfer?transferId=— Asientos de una transferenciaGET /api/ledger/daily-report?date=— Reporte diario (saldos apertura/cierre, débitos, créditos)GET /api/ledger/balance?accountNumber=— Balance calculado de una cuentaGET /api/ledger/stream?token=<jwt>— SSE en tiempo real (solo admin)
Flujo: Consume bank.account.events vía Kafka → persiste asientos DR/CR en MySQL → push vía SSE. Solo admin puede consultar.
Notificaciones en tiempo real con SSE.
GET /api/notifications— Listar notificaciones del usuario (paginado)GET /api/notifications/unread-count— Conteo de no leídasPATCH /api/notifications/{id}/read— Marcar como leídaPATCH /api/notifications/read-all— Marcar todas como leídasGET /api/notifications/stream?token=<jwt>— SSE en tiempo real
Flujo: Consume bank.notification.events vía Kafka → persiste en MySQL → push inmediato al navegador vía SseEmitter. No envía emails ni SMS.
Puerta de entrada única (Spring Cloud Gateway).
| Ruta | Destino |
|---|---|
/api/auth/** |
users-service (público) |
/api/users/** |
users-service |
/api/admin/** |
users-service |
/api/accounts/** |
accounts-service |
/api/cards/** |
accounts-service |
/api/transfers/** |
transfers-service |
/api/ledger/** |
ledger-service |
/api/notifications/** |
notifications-service |
Valida el JWT en cada petición entrante (excepto /api/auth/**) e inyecta las cabeceras X-User-Id y X-User-Role hacia los microservicios.
Interfaz de usuario en React 19 + TypeScript + Vite + Tailwind CSS 4.
| Ruta | Vista |
|---|---|
/ |
Login con selección de rol |
/login/cliente |
Login cliente |
/login/admin |
Login admin |
/registro |
Registro de usuario |
/dashboard |
Panel principal (tarjetas → cuentas → transferencias recientes) |
/accounts/:id |
Detalle de cuenta + movimientos |
/admin |
Panel de administración |
Dashboard:
- Sección de tarjetas primero (emitir, cambiar PIN, bloquear)
- Sección de cuentas después (crear vinculada a tarjeta, depositar, transferir)
- Depósito con tarjeta: requiere selección de cuenta vinculada,
pin4ypin6 - Transferencia externa con tarjeta: requiere
pin4y selección de cuenta origen vinculada
Librería compartida (JAR plano, no Spring Boot) con DTOs, eventos de Kafka, componentes de seguridad y utilidades usadas por todos los microservicios. Incluye:
- Eventos:
TransferRequestedEvent,AccountDebitedEvent,AccountCreditedEvent,AccountRejectedEvent,TransferCompletedEvent,TransferFailedEvent,AccountCreatedEvent,AccountDepositedEvent,UserCreatedEvent,TransferNotificationEvent - Seguridad:
JwtTokenValidator,JwtAuthFilter— validación JWT en cada servicio - Utilidades:
ApiResponse<T>,Result<T>,ResponseHelper, interfaces genéricas
Proxy reverso que expone el sistema completo en el puerto 8888. Enruta /api/ hacia el api-gateway, sirve Swagger UI y redirige el resto al frontend.
User
├── Emite tarjeta (POST /api/cards/issue)
│ → pin4 (4 dígitos) + pin6 (6 dígitos)
│ → PAN generado: 400000 + 10 dígitos random
│ → Expiración: 5 años desde emisión
│
├── Crea cuenta (POST /api/accounts/create)
│ → Selecciona moneda (USD/EUR/PEN)
│ → Selecciona tarjeta para vincular
│ → La primera cuenta vinculada es la "principal"
│
├── Deposita con tarjeta (POST /api/cards/{id}/deposit)
│ → Selecciona cuenta vinculada a la tarjeta
│ → Ingresa pin4 + pin6
│ → Valida: tarjeta activa, no expirada, PIN correcto, cuenta activa, vínculo existente
│
└── Transfiere con tarjeta (POST /api/transfers/card-payment)
→ Selecciona cuenta origen vinculada a la tarjeta
→ Ingresa pin4 + cuenta destino
→ Misma validación de tarjeta + cuenta origen
- Depósito: rechazado si la cuenta destino está
BLOCKED(tanto directo como con tarjeta) - Transferencia interna: rechazada si origen O destino está
BLOCKED(pre-debit) - Transferencia externa: si destino está
BLOCKED, se revierte el débito automáticamente (rollback)
- Solo se pueden usar tarjetas
ACTIVE - Tarjetas expiradas son rechazadas
pin4ypin6validados contra valores almacenados- Cuenta destino debe estar vinculada a la tarjeta (tabla
card_accounts)
El sistema garantiza que una misma operación ejecutada múltiples veces produzca el mismo resultado sin efectos secundarios (safe retry). La implementación cubre tres niveles:
Todos los POST de transfers-service (/api/transfers/internal, /external, /card-payment) requieren la cabecera Idempotency-Key con un UUID.
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Flujo:
- El servicio busca si ya existe un transfer con ese UUID como
transferId - Si existe → devuelve
200 OKcon los mismos datos (idempotent replay) - Si no existe → crea el transfer usando ese UUID como
transferIdy devuelve201 Created - La UK (
transfer_idúnico en tablatransfers) previene duplicados incluso en race conditions
Si dos requests idénticos llegan simultáneamente, el segundo puede recibir un
409 Conflict(unique constraint). El cliente debe reintentar con la misma key para obtener el replay.
Cada consumer tiene su propia estrategia de detección de duplicados usando el transferId que viaja en todos los eventos:
| Consumer | Estrategia |
|---|---|
accounts-service TransferEventConsumer |
Verifica si ya existe un AccountMovement con ese transferId (UK en account_movements.transfer_id). Si existe, ignora el evento. |
ledger-service AccountEventConsumer |
Verifica si ya existe un entry con la combinación (transfer_id, entry_type, account_number) (UK compuesta). Si existe, ignora. |
transfers-service AccountEventConsumer |
Verifica el status actual del transfer: solo procesa eventos si el transfer está en el estado esperado (PENDING para débito/rechazo, DEBITED para crédito). |
El patrón outbox en transfers-service provee al menos una vez de publicación a Kafka. El poller reintenta eventos FAILED/PENDING periódicamente. La idempotencia en los consumers absorbe los duplicados que pudieran generarse por estos reintentos.
| Componente | Tecnología |
|---|---|
| Base de datos | MySQL 8 (5 bases: bank_users, bank_accounts, bank_transfers, bank_ledger, bank_notifications) |
| Mensajería | Apache Kafka + Zookeeper |
| UI de Kafka | Kafka UI (provectuslabs/kafka-ui) en :8080 |
| Compilación | Maven (Java 21) |
| Contenedores | Docker Compose |
| Comando | Descripción |
|---|---|
make up |
Levantar todo el stack (docker compose up -d --build) |
make down |
Detener todos los contenedores |
make restart |
Reiniciar todo |
make logs |
Ver logs de todos los servicios |
make ps |
Estado de los contenedores + URLs de acceso |
make infra |
Solo MySQL + Zookeeper + Kafka (para desarrollo local) |
make install |
Compilar e instalar shared-contracts en .m2 local |
make run-accounts |
Compilar shared + ejecutar accounts-service en :8082 |
make run-transfers |
Compilar shared + ejecutar transfers-service en :8083 |
make run-ledger |
Compilar shared + ejecutar ledger-service en :8084 |
make run-notifications |
Compilar shared + ejecutar notifications-service en :8085 |
make run-users |
Compilar shared + ejecutar users-service en :8081 |
make run-gateway |
Ejecutar api-gateway en :8000 |
make createsuperuser |
Crear usuario administrador vía API |
Para trabajar en un solo servicio sin Docker Compose completo:
# 1. Levantar solo infraestructura (MySQL + Kafka)
make infra
# 2. En otra terminal, arrancar el servicio que necesites
make run-accounts
# 3. Si necesitas el API Gateway para rutas unificadas
make run-gatewayLos servicios se auto-configuran gracias a
application.yamlcon defaults locales. Kafka debe estar corriendo para que el servicio inicie correctamente. Los topics se crean automáticamente conmake infra.
| Servicio | Estado |
|---|---|
| users-service | ✅ Completamente implementado |
| accounts-service | ✅ Completamente implementado (cuentas, tarjetas pin4+pin6, depósitos, movimientos) |
| transfers-service | ✅ Completamente implementado (Outbox + Idempotencia) |
| api-gateway | ✅ Completamente implementado |
| frontend | ✅ Funcionalidades principales implementadas |
| shared-contracts | ✅ Completamente implementado |
| ledger-service | ✅ Completamente implementado (Kafka → MySQL → SSE, solo admin) |
| notifications-service | ✅ Completamente implementado (Kafka → MySQL → SSE) |
| CI/CD (GitHub Actions) | ❌ Pendiente |
| Currency Exchange Service | 📋 Planificado |
Las variables de configuración están organizadas en archivos .env (excluidos de Git) por servicio, más un .env raíz para infraestructura compartida.
.env ← infraestructura compartida (MySQL, Kafka, etc.)
.env.example ← ejemplo del archivo raíz
users-service/.env ← datasource, JWT, admin secrets
accounts-service/.env ← datasource, JWT secret
transfers-service/.env ← datasource
ledger-service/.env ← datasource
notifications-service/.env ← datasource
api-gateway/.env ← JWT secret
./.env (compartido — inyectado a todos los servicios)
| Variable | Descripción | Default |
|---|---|---|
MYSQL_ROOT_PASSWORD |
Contraseña root de MySQL | rootpass |
DB_USERNAME |
Usuario de BD | root |
DB_PASSWORD |
Contraseña de BD | rootpass |
JPA_DDL_AUTO |
Estrategia DDL de Hibernate | update |
KAFKA_BOOTSTRAP_SERVERS |
Servidores Kafka | kafka:9092 |
DOCKER_COMPOSE_ENABLED |
Auto-config de Docker Compose | false |
API_PROXY_TARGET |
Target del proxy de Vite | http://api-gateway:8000 |
./{servicio}/.env (específico por servicio)
| Servicio | Variables |
|---|---|
| users-service | USERS_DATASOURCE_URL, JWT_SECRET, JWT_ACCESS_EXPIRATION_MS, JWT_REFRESH_EXPIRATION_MS, ADMIN_BOOTSTRAP_SECRET |
| accounts-service | ACCOUNTS_DATASOURCE_URL, JWT_SECRET |
| transfers-service | TRANSFERS_DATASOURCE_URL |
| ledger-service | LEDGER_DATASOURCE_URL |
| notifications-service | NOTIFICATIONS_DATASOURCE_URL |
| api-gateway | JWT_SECRET |
- Copia los archivos de ejemplo en cada carpeta:
cp .env.example .env cp users-service/.env.example users-service/.env cp accounts-service/.env.example accounts-service/.env cp transfers-service/.env.example transfers-service/.env cp ledger-service/.env.example ledger-service/.env cp notifications-service/.env.example notifications-service/.env cp api-gateway/.env.example api-gateway/.env
- Ajusta los valores según tu entorno (especialmente
JWT_SECRETyMYSQL_ROOT_PASSWORDen producción) - Los archivos
.envson leídos automáticamente por Docker Compose (make up)
Nota: Los
application.yamlde cada servicio tienen valores por defecto que funcionan localmente mediantemvnw spring-boot:run. Las variables del.envsobreescriben esos defaults solo cuando se despliega con Docker.
- Backend: Java 21, Spring Boot 4.0.6, Spring Cloud 2025.1.1
- Frontend: React 19, TypeScript, Vite 8, Tailwind CSS 4
- Base de datos: MySQL 8
- Mensajería: Apache Kafka 7.7.0
- Seguridad: JWT (jjwt 0.12.6), BCrypt
- Infraestructura: Docker, Docker Compose, nginx