TinerPay

Plataforma distribuida de pagos digitales

Caso práctico de arquitectura distribuida utilizando CockroachDB para garantizar consistencia, alta disponibilidad y escalabilidad.

Explorar proyecto ↓

¿Qué es TinerPay?

TinerPay es una plataforma fintech que permite a los usuarios crear cuentas digitales y gestionar wallets en distintas divisas. Los usuarios pueden enviar y recibir dinero en tiempo real mediante transferencias seguras registradas en una base de datos distribuida. El sistema opera entre Europa y Latinoamérica, donde la consistencia de los datos y la disponibilidad continua son críticas.

👤

Usuario

Crea una cuenta

💰

Wallet

Gestiona saldo

🔁

Transferencia

Envía dinero

El límite de las arquitecturas centralizadas

TinerPay opera entre Europa y Latinoamérica con millones de transacciones financieras simultáneas. Una arquitectura centralizada no puede soportar esto.

Punto único de fallo: si cae el servidor, cae toda la plataforma.

Cuello de botella: un solo servidor no escala con millones de operaciones simultáneas.

Latencia geográfica: un servidor en Europa responde lento para usuarios en Latinoamérica.
Arquitectura centralizada

La base de datos diseñada para el entorno transaccional distribuido

Exigencias críticas

(TinerPay)

CockroachDB bridge
CockroachDB

Integridad financiera

CockroachDB resuelve los tres problemas anteriores de forma nativa: replica los datos entre nodos eliminando el punto único de fallo, escala horizontalmente añadiendo nodos al cluster sin interrupciones, y distribuye los datos por regiones reduciendo la latencia para usuarios en Europa y Latinoamérica.

Por qué CockroachDB

Consistencia fuerte

Las transacciones financieras deben ejecutarse sin pérdida ni duplicación de datos.

Consistencia de datos

Alta disponibilidad

Los datos se replican automáticamente entre nodos del cluster.

Alta disponibilidad

Escalabilidad horizontal

Permite añadir nodos al cluster para soportar más usuarios.

Escalabilidad horizontal

Distribución multi-región

Reduce la latencia para usuarios en diferentes regiones.

Multi-región

Análisis de requisitos

Requisitos funcionales

Funcionales

  • Crear: usuarios, wallets, transacciones, divisas
  • Consultar: saldo de una wallet, historial de transacciones, datos de usuario
  • Modificar: saldo de wallet, datos de usuario
  • Eliminar: transacciones, wallets, usuarios
  • Transferir: dinero entre wallets de distintos usuarios

No funcionales

  • Alta disponibilidad 24/7
  • Consistencia fuerte (ACID)
  • Persistencia de datos
  • Escalabilidad horizontal
  • Baja latencia global
  • Tolerancia a fallos de nodos
Requisitos no funcionales

Modelo de datos

El sistema utiliza un modelo relacional compatible con SQL. A continuación se identifican las cuatro entidades principales con sus campos, claves primarias (PK) y claves foráneas (FK).

👤 Usuario como entidad central 💳 Cada wallet opera en una divisa 🔁 Historial completo y trazable

👤 Usuario

  • id (PK · UUID)
  • name
  • email (UNIQUE)
  • deleted_at (soft delete)
  • created_at

💱 Currency

  • code (PK · STRING)
  • name
  • symbol

💳 Wallet

  • id (PK · UUID)
  • balance
  • currency_code (FK → currencies)
  • user_id (FK → users)
  • created_at

🔁 Transaction

  • id (PK · UUID)
  • from_wallet (FK → wallets)
  • to_wallet (FK → wallets)
  • amount
  • created_at
  • CHECK from ≠ to

Relaciones entre entidades

Una vez identificadas las entidades, se definen las relaciones entre ellas para construir el modelo relacional completo.

👤 Usuario Wallet

Un usuario puede poseer múltiples wallets dentro del sistema.

1 : N

💱 Currency Wallet

Una divisa puede estar asociada a múltiples wallets.

1 : N

💳 Wallet Transaction

Una wallet puede participar en múltiples transacciones como origen o destino.

1 : N

Estructura de la base de datos

Inicialización y definición de tablas en CockroachDB

⚙️ Inicialización
init.sql

Crea la base de datos y la selecciona como contexto activo.

CREATE DATABASE tinerpay;

USE tinerpay;
schema.sql

Entidad central. Añadimos deleted_at para soft delete y mantener historial completo.

CREATE TABLE users (
  id         UUID      PRIMARY KEY DEFAULT gen_random_uuid(),
  name       STRING    NOT NULL,
  email      STRING    UNIQUE NOT NULL,
  deleted_at TIMESTAMP NULL,
  created_at TIMESTAMP DEFAULT now()
);

Catálogo de divisas. Referenciada por wallets para garantizar integridad referencial.

CREATE TABLE currencies (
  code   STRING PRIMARY KEY,
  name   STRING NOT NULL,
  symbol STRING NOT NULL
);

Sin ON DELETE CASCADE — evitamos borrar wallets si se elimina un usuario.

CREATE TABLE wallets (
  id            UUID      PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id       UUID      REFERENCES users(id),
  currency_code STRING    REFERENCES currencies(code),
  balance       DECIMAL   NOT NULL DEFAULT 0 CHECK (balance >= 0),
  created_at    TIMESTAMP DEFAULT now()
);

Sin CASCADE en FK — el historial financiero no se borra bajo ningún concepto.

CREATE TABLE transactions (
  id          UUID      PRIMARY KEY DEFAULT gen_random_uuid(),
  from_wallet UUID      REFERENCES wallets(id),
  to_wallet   UUID      REFERENCES wallets(id),
  amount      DECIMAL   NOT NULL CHECK (amount > 0),
  created_at  TIMESTAMP DEFAULT now(),
  CHECK (from_wallet != to_wallet)
);

Índices básicos

Los índices aceleran las consultas más frecuentes del sistema. Las claves primarias y el campo email (UNIQUE) ya tienen índice automático. Los siguientes se crean explícitamente sobre las claves foráneas.

🔍 Wallets por usuario

CREATE INDEX ON wallets (user_id);

Permite consultar todas las wallets de un usuario sin recorrer toda la tabla.

🔍 Transacciones por wallet origen

CREATE INDEX ON transactions (from_wallet);

Acelera la consulta del historial de envíos de una wallet concreta.

🔍 Transacciones por wallet destino

CREATE INDEX ON transactions (to_wallet);

Acelera la consulta del historial de recepciones de una wallet concreta.

🔍 Wallets por divisa

CREATE INDEX ON wallets (currency_code);

Útil para filtrar wallets en una divisa específica (EUR, USD, etc.).

ℹ️ En CockroachDB los UUID como PK ya están indexados internamente. Solo se crean índices adicionales sobre los campos de búsqueda frecuente.

Operaciones SQL

tinerpay.sql

Crear usuario con wallet inicial — se usa un CTE para obtener el UUID generado y asociarlo a la wallet en una sola operación atómica.

BEGIN;

WITH new_user AS (
  INSERT INTO users (name, email)
  VALUES ('Diego', 'diego@gmail.com')
  RETURNING id
)
INSERT INTO wallets (user_id, currency_code, balance)
SELECT id, 'EUR', 0 FROM new_user;

COMMIT;

Balances de un usuario activo — usa el índice wallets(user_id).

SELECT w.balance, w.currency_code
FROM wallets w
JOIN users u ON w.user_id = u.id
WHERE u.email = 'diego@gmail.com'
AND u.deleted_at IS NULL;

Historial de envíos de una wallet — usa el índice transactions(from_wallet).

SELECT t.amount, t.created_at
FROM transactions t
WHERE t.from_wallet = 'uuid-wallet-origen'
ORDER BY t.created_at DESC;

Historial de recepciones de una wallet — usa el índice transactions(to_wallet).

SELECT t.amount, t.created_at
FROM transactions t
WHERE t.to_wallet = 'uuid-wallet-destino'
ORDER BY t.created_at DESC;

Wallets por divisa — usa el índice wallets(currency_code).

SELECT w.id, w.balance, u.name
FROM wallets w
JOIN users u ON w.user_id = u.id
WHERE w.currency_code = 'EUR'
AND u.deleted_at IS NULL;

JOIN con users en la propia query — solo actualiza wallets de usuarios activos.

UPDATE wallets w
SET balance = balance + 100
FROM users u
WHERE w.user_id = u.id
AND u.email = 'diego@gmail.com'
AND u.deleted_at IS NULL;

Soft delete — marcamos deleted_at en lugar de borrar, preservando el historial financiero completo.

UPDATE users
SET deleted_at = now()
WHERE email = 'diego@gmail.com'
AND deleted_at IS NULL;

Arquitectura de TinerPay

App TinerPay en un móvil
TinerPay
📶 🔔
Cuentas Acciones
Euro
120,00
Transferencia a Pedro
Hoy · 18:32
-50€
Pago Mamá
Ayer · 22:14
-12€
Ingreso Abuela
2 Feb
+120€
API CORE

API Backend

  • 👤 Usuarios
  • 💳 Wallets
  • 🔄 Transacciones
Nodo EU
Nodo US
Nodo LATAM
Mascota de CockroachDB

Preguntas frecuentes

Análisis de requisitos

¿Puede un usuario tener varias wallets?

Sí. Un usuario puede poseer múltiples wallets asociadas a su cuenta.

¿Qué significa ACID?

Atomicidad, Consistencia, Aislamiento y Durabilidad.

¿Qué es CockroachDB?

Una base de datos SQL distribuida con transacciones ACID y alta disponibilidad.

¿Qué pasa si falla un nodo del cluster?

Los otros nodos siguen operando. El sistema no cae gracias al consenso Raft.

¿Por qué se usa UUID como clave primaria?

Garantiza unicidad en un sistema distribuido sin riesgo de colisión entre nodos.

¿Qué hace el CHECK en la tabla wallets?

Garantiza que el balance nunca sea negativo.

¿Por qué no puedes borrar un usuario directamente?

Por obligación legal, el historial financiero debe ser inmutable y trazable.

¿Qué es un índice y para qué sirve?

Acelera las consultas más frecuentes sin recorrer toda la tabla.

Instalación del cluster

El entorno se despliega utilizando Docker Compose.


            

Cómo funciona el backend y cómo se conecta con la web

Flujo completo de una acción (ejemplo: crear usuario)

1. FRONTEND (WEB)
TinerPay

El usuario pulsa un botón en la página web.

PETICIÓN HTTP
(fetch)
2. BACKEND (Node.js + Express)
API REST (RUTAS)
POST /api/users Crear usuario
GET /api/users Listar usuarios
POST /api/transactions Hacer transferencia
GET /api/cluster/status Estado del cluster
...

El backend recibe la petición, ejecuta la lógica correspondiente y realiza consultas a la base de datos.

CONSULTA SQL
3. BASE DE DATOS DISTRIBUIDA
(CockroachDB — 3 NODOS)
— — — REPLICACIÓN (RAFT) — — —
Nodo 1
:26257
Nodo 2
:26258
Nodo 3
:26259

CockroachDB replica los datos en los 3 nodos usando el consenso Raft para mantener todo consistente y disponible.

←   RESPUESTA JSON   ←
FLUJO PASO A PASO
1
👆
Acción del usuario
El usuario hace click en un botón o rellena un formulario.
2
🌐
Petición HTTP
La web envía una petición HTTP al backend usando fetch().
3
🗄️
Backend recibe
Express recibe la petición en la ruta correspondiente.
4
⚙️
Lógica + SQL
El backend valida los datos y ejecuta una o varias consultas SQL.
5
🗃️
Base de datos
CockroachDB procesa la consulta y replica los cambios (Raft).
6
↩️
Respuesta
La base de datos devuelve el resultado y el backend lo envía como JSON.
7
UI actualizada
La web recibe la respuesta y actualiza la interfaz sin recargar la página.
CONEXIÓN INTELIGENTE (FALLBACK)
1
El backend intenta conectarse al Nodo 1 (26257).
2
Si falla o no responde, prueba el Nodo 2 (26258).
3
Si también falla, prueba el Nodo 3 (26259).
💡 Así el sistema sigue funcionando aunque un nodo se caiga.
¿QUÉ ES EL BACKEND?

Es el intermediario entre la web y la base de datos. Se encarga de la lógica, seguridad, validaciones y acceso controlado a los datos.

¿POR QUÉ ES IMPORTANTE?
  • Seguridad: la base de datos no es accesible directamente.
  • Control: toda la lógica de negocio está en el backend.
  • Resiliencia: si un nodo falla, el sistema sigue funcionando.
TECNOLOGÍAS UTILIZADAS
  • Frontend: HTML, CSS, JavaScript
  • Backend: Node.js, Express
  • Base de datos: CockroachDB (distribuida, Raft)
  • Comunicación: HTTP + JSON

Demo interactiva


            

            

Nodo 1

● LIVE
Range 1 • Follower
Range 2 • Follower
Range 3 • Leader
Puerto SQL26257
HTTP UI:8080 ↗
Réplicas3
ConsensoRaft
Heartbeat

Nodo 2

● LIVE
Range 1 • Leader
Range 2 • Follower
Range 3 • Follower
Puerto SQL26258
HTTP UI:8081 ↗
Réplicas3
ConsensoRaft
Heartbeat

Nodo 3

● LIVE
Range 1 • Follower
Range 2 • Leader
Range 3 • Follower
Puerto SQL26259
HTTP UI:8082 ↗
Réplicas3
ConsensoRaft
Heartbeat
Cluster Event Log