ACID es un contrato, no una religion: Como los sistemas reales protegen el dinero
La mayoria de los ingenieros aprenden ACID temprano. La mayoria tambien aprenden despues que ACID no es toda la historia. Eso no es una contradiccion -- es un problema de alcance. Este articulo es una guia practica de arquitectura sobre libros contables de partida doble, control de concurrencia, niveles de aislamiento y la disciplina silenciosa que separa los sistemas de pago que funcionan de los sistemas de pago que funcionan hasta que dejan de hacerlo.
Tabla de Contenidos
La mayoria de los ingenieros aprenden ACID temprano. La mayoria tambien aprenden despues que ACID no es toda la historia.
Eso no es una contradiccion. Es un problema de alcance.
ACID es un contrato para una transaccion de base de datos en un sistema de base de datos. El movimiento de dinero es un contrato a traves del tiempo, redes, servicios, personas, reguladores y atacantes. La correccion en produccion vive por encima de la base de datos. La usa. No la venera.
Este articulo es una guia practica y teorica. Cubre ACID. Cubre concurrencia. Cubre seguridad. Y se enfoca en el unico diseno que gana repetidamente en finanzas: el libro contable.
I. Invariantes primero, herramientas despues
Correccion significa que tus invariantes se mantienen. No "la base de datos hizo commit."
En pagos y banca, las invariantes tipicas lucen asi:
- El dinero no puede aparecer ni desaparecer dentro de tu sistema.
- Cada transferencia contabilizada esta balanceada.
- Un asiento contabilizado nunca se edita. Solo se compensa con un asiento posterior.
- Una transaccion se procesa como maximo una vez, incluso si las solicitudes se repiten.
- Los "fondos disponibles" nunca bajan de los limites de politica.
- Cada saldo puede explicarse desde una pista de auditoria.
Observa lo que falta. Ninguna invariante dice "usamos aislamiento serializable." El aislamiento es una herramienta. Las invariantes son el objetivo. Ese encuadre hace que ACID sea mas facil de razonar. Tambien hace mas facil admitir donde termina ACID.
II. Lo que ACID garantiza -- y donde termina
ACID sigue siendo esencial. Pero es mas estrecho de lo que muchos equipos suponen.
Atomicidad. Todas las escrituras en una transaccion se confirman o ninguna lo hace. La garantia de "sin actualizacion parcial." No negociable para contabilizacion en el libro contable.
Consistencia. Las restricciones que defines se aplican -- claves foraneas, checks, restricciones unicas, triggers. Esto es "correccion de reglas de base de datos," no "correccion de negocio." La distincion importa mas de lo que la mayoria de los arquitectos admiten.
Aislamiento. Las transacciones concurrentes se comportan como si se ejecutaran con reglas de interferencia definidas. Las reglas dependen del nivel de aislamiento. Muchos valores por defecto en produccion son mas debiles de lo que los ingenieros creen (Berenson et al., 1995).
Durabilidad. Los datos confirmados sobreviven a fallos. Dentro de los supuestos de almacenamiento y replicacion.
Ahora, la brecha. ACID no garantiza correccion a traves de multiples servicios, caches, replicas de lectura, brokers de mensajes, reintentos, timeouts o fallos parciales de red. Tampoco garantiza tus invariantes de negocio -- a menos que las codifiques, y las codifiques de una manera que la concurrencia no pueda romper.
Esa es la parte dificil.
Los niveles de aislamiento tratan sobre anomalias, no sobre sensaciones
El estandar SQL define niveles de aislamiento segun que anomalias se permiten. El articulo clasico que aclaro la confusion es A Critique of ANSI SQL Isolation Levels de Berenson et al. (1995). Antiguo. Aun vale la pena leerlo. Porque los modos de fallo son atemporales.
Las anomalias que deberias temer en codigo de dinero:
- Lost update: Dos debitos leen el mismo saldo y ambos tienen exito.
- Write skew: Dos transacciones mantienen restricciones localmente pero violan una invariante global en conjunto.
- Phantoms: Una consulta de rango cambia bajo tus pies, rompiendo la logica de "verificar y luego actuar."
- Read skew: Una transaccion lee filas relacionadas de diferentes puntos en el tiempo.
El snapshot isolation (SI) es especialmente traicionero. Es atractivo. Escala. Pero SI puede permitir write skew -- dos transacciones ven cada una un mundo donde una restriccion se mantiene, ambas confirman escrituras no conflictivas, y la invariante global se rompe (Berenson et al., 1995).
Si quieres el modelo mental simple: serializabilidad. El efecto equivale a algun orden serial. PostgreSQL documenta esto claramente. Pero serializable tiene un costo. Asi que los sistemas de alta escala no siempre ejecutan "serializable en todas partes." En su lugar, hacen la serializabilidad pequena. Reducen el dominio de contencion.
Este es el comienzo del pensamiento contable.
III. El movimiento del libro contable: Estado derivado sobre estado mutable
Muchos sistemas comienzan con una columna accounts.balance. Luego hacen: leer saldo, verificar fondos suficientes, escribir nuevo saldo.
Esta es la fabrica de bugs de concurrencia. Puede parchearse con locks, reintentos, SQL cuidadoso. Pero el modelo central es fragil.
Un modelo de libro contable invierte la direccion:
- Almacenas asientos inmutables.
- Calculas saldos como una suma de asientos.
- Tratas el resultado calculado como estado derivado.
Tres propiedades llegan inmediatamente: una pista de auditoria por defecto, la capacidad de reconstruir estado derivado despues de errores, y la capacidad de razonar sobre correccion a lo largo del tiempo.
Un libro contable real de grado bancario es usualmente de partida doble. Esto no es tradicion. Es algebra. El articulo de Griffin sobre el "banco inmutable" lo dice claramente: los asientos de diario tienen lineas, y la suma de debitos es igual a la suma de creditos (Griffin, 2022). Esa invariante previene "dinero perdido." No previene "destinatario equivocado." Pero garantiza la conservacion dentro del sistema.
TigerBeetle, una base de datos moderna de libro contable financiero, expresa la misma invariante como una primitiva de primera clase. Diferente implementacion. Misma matematica.
Anatomia del libro contable: cuentas, asientos de diario, movimientos
Tres conceptos centrales:
- Cuenta: un contenedor de valor en una moneda o tipo de activo.
- Asiento de diario: un registro de transaccion que porta metadatos.
- Movimiento (linea): un debito o credito aplicado a una cuenta, vinculado a un asiento de diario.
Esquema relacional minimo:
CREATE TABLE accounts (
account_id BIGINT PRIMARY KEY,
currency CHAR(3) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
closed_at TIMESTAMPTZ
);
CREATE TABLE journal_entries (
entry_id UUID PRIMARY KEY,
idempotency_key TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
posted_at TIMESTAMPTZ,
status TEXT NOT NULL CHECK (status IN
('PENDING','POSTED','REVERSED','FAILED')),
type TEXT NOT NULL,
meta JSONB NOT NULL DEFAULT '{}'
);
CREATE TABLE postings (
posting_id BIGSERIAL PRIMARY KEY,
entry_id UUID NOT NULL REFERENCES journal_entries(entry_id),
account_id BIGINT NOT NULL REFERENCES accounts(account_id),
side TEXT NOT NULL CHECK (side IN ('DEBIT','CREDIT')),
amount_minor BIGINT NOT NULL CHECK (amount_minor > 0),
currency CHAR(3) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX postings_account_time ON postings(account_id, created_at);
CREATE INDEX postings_entry ON postings(entry_id);
La invariante: para cada asiento de diario POSTED, por moneda, sum(debitos) = sum(creditos). Aplica esto en un procedimiento almacenado de contabilizacion o con restricciones diferidas. No confies solo en la disciplina del desarrollador.
Ya no estas protegiendo "un numero de saldo." Estas protegiendo una ley de conservacion.
IV. Control de concurrencia: Hacer la serializabilidad pequena
Contabilizar un asiento de diario y sus movimientos debe ser atomico. Ahi es donde ACID brilla. Una transaccion: deduplicar via idempotency key, validar reglas de negocio, agregar movimientos, marcar el asiento como contabilizado, emitir un evento outbox. Si se confirma, el libro esta actualizado. Si se revierte, nada paso.
Hasta aqui, esto es "ACID bien hecho." Pero la concurrencia es donde los sistemas se desmoronan.
Una base de datos global serializable es costosa. Pero la mayoria de las invariantes de dinero son locales -- por cuenta, por par de cuentas, por contenedor "disponible" vs "pendiente." Asi que aplicas comportamiento serial bloqueando las filas minimas.
Enfoque A: Locks pesimistas deterministicos
Al mover valor de la cuenta A a la cuenta B, bloquea ambas en orden para evitar deadlocks:
BEGIN;
-- Orden de bloqueo por account_id para prevenir deadlocks
SELECT 1 FROM accounts
WHERE account_id = LEAST($from, $to) FOR UPDATE;
SELECT 1 FROM accounts
WHERE account_id = GREATEST($from, $to) FOR UPDATE;
-- Insertar asiento (idempotente)
INSERT INTO journal_entries(entry_id, idempotency_key,
status, type, meta)
VALUES ($entry_id, $idem, 'PENDING', 'TRANSFER', $meta)
ON CONFLICT (idempotency_key) DO UPDATE
SET entry_id = journal_entries.entry_id
RETURNING entry_id;
-- Agregar movimientos (balanceados)
INSERT INTO postings(entry_id, account_id, side,
amount_minor, currency)
VALUES
($entry_id, $from, 'DEBIT', $amt, $cur),
($entry_id, $to, 'CREDIT', $amt, $cur);
UPDATE journal_entries
SET status = 'POSTED', posted_at = now()
WHERE entry_id = $entry_id;
COMMIT;
Simple. Poderoso. Esto te da ejecucion serial por cuenta incluso en Read Committed. Los bloqueos de fila son un instrumento contundente que previene write skew. PostgreSQL tambien ofrece serializable via SSI -- pero aun asi, planifica reintentos.
Enfoque B: Concurrencia optimista con reintentos
Muchas bases de datos distribuidas usan serializable con reintentos por defecto. CockroachDB ejecuta serializable por defecto y expone errores de reintento. FoundationDB proporciona serializabilidad estricta usando control de concurrencia optimista.
El costo es operacional. Tu aplicacion debe reintentar de forma segura. Tus operaciones deben ser idempotentes. Tus clientes deben tolerar picos de latencia bajo contencion.
Elige uno: bloqueas y esperas, o ejecutas optimista y reintentas. Ambos pueden ser correctos. Ambos tienen costos. Si no puedes reintentar de forma segura, no pretendas que puedes. Usa locks.
La trampa del snapshot isolation
Si tu invariante depende de un predicado -- no de una sola fila -- entonces el snapshot isolation puede romperla silenciosamente. Dos transacciones ven cada una un mundo donde una restriccion se mantiene, ambas confirman escrituras no conflictivas, y la invariante global se quiebra.
Tres soluciones:
- Bloquear el rango del predicado.
- Ejecutar serializable verdadero.
- Codificar la invariante como una "puerta" de una sola fila que puedes bloquear.
Los sistemas de libro contable suelen usar la tercera opcion. Fuerzan las invariantes a un dominio pequeno y bloqueable. El articulo de SSI de Ports y Grittner describe como PostgreSQL detecta estructuras peligrosas para prevenir anomalias bajo SERIALIZABLE.
V. Liquidacion, reservas y el patron de dos fases
La contabilizacion interna debe ser limpia. La liquidacion externa es desordenada.
Griffin separa explicitamente el libro contable de una capa "transactor" que rastrea estados como instruccion, retenido, fallido, completado (Griffin, 2022). Ese patron es comun. Existe porque las redes fallan. Los rieles de pago expiran. Las contrapartes discrepan. Las personas presentan disputas.
Asi que modelas dos cosas distintas:
- Contabilizacion en el libro: verdad contable interna.
- Flujo de liquidacion: convergencia con la realidad externa.
No contabilices asientos finales en el libro para transferencias externas hasta que sepas que significan. Usa retenciones pendientes en su lugar.
[Interno] [Externo]
+-----------------------+ +------------------------+
| Libro contable | | Flujo de liquidacion |
| (inmutable) | | |
| - asientos_diario |<----->| - INSTRUIDO |
| - movimientos | | - RETENIDO |
| - saldos_cuenta | | - COMPLETADO / FALLIDO|
+-----------------------+ +------------------------+
TigerBeetle llama a esto "pending transfers." Griffin lo llama fondos "held." Las redes de tarjetas lo llaman autorizacion vs captura. Diferentes nombres. Mismo concepto.
Un modelo practico: disponible (gastable ahora) y pendiente (reservado para una accion en curso). Una autorizacion de tarjeta mueve valor de disponible a pendiente. Una captura mueve pendiente a contabilizado final. Una anulacion mueve pendiente de vuelta a disponible.
Esto tiene dos beneficios: previenes el sobregasto mientras la liquidacion externa es incierta, y puedes mostrar UX veraz. "Pendiente" no es un error. Es honestidad.
VI. Mas alla de la base de datos unica: Sagas, outboxes, idempotencia
Puedes construir un sistema de commit de dos fases distribuido. Tambien puedes construir una maquina de deadlocks distribuida.
La mayoria de las arquitecturas modernas evitan transacciones distribuidas entre servicios. Usan transacciones locales mas flujos de trabajo.
Patron 1: Saga
Una saga es una secuencia de transacciones locales. Cada paso confirma localmente y dispara el siguiente paso via mensajes. Si un paso falla, se ejecutan transacciones compensatorias (Microservices.io, Saga Pattern). Las sagas no son "consistencia eventual a mano alzada." Son explicitas. Son auditables. Requieren fuerte disciplina de idempotencia.
Patron 2: Outbox transaccional
El problema de dual-write es real. Actualizas la base de datos. Publicas en Kafka. Uno tiene exito. Otro falla. Y ahora que? Confluent describe este modo de fallo claramente.
La solucion es el outbox transaccional: en la misma transaccion de base de datos que tu escritura de negocio, insertas un "evento" en una tabla outbox. Un publicador separado lee el outbox y publica de forma confiable. Si la base de datos confirma, el mensaje eventualmente se publica. Si la base de datos revierte, no existe ningun mensaje.
Esto convierte "atomicidad distribuida" en "atomicidad local + reenvio confiable." Es aburrido. Por eso funciona.
Ruta de escritura (transaccion de BD unica)
+-----------------------------------------+
| 1. Insertar asiento de diario |
| 2. Insertar movimientos balanceados |
| 3. Actualizar saldo en cache |
| 4. Insertar evento outbox |
| 5. Marcar asiento como CONTABILIZADO |
+-----------------------------------------+
|
v
Lector de outbox (asincrono)
+-----------------------------------------+
| Leer eventos no enviados -> Publicar |
| Marcar eventos como enviados |
+-----------------------------------------+
Idempotencia sobre exactamente-una-vez
La entrega "exactamente una vez" de extremo a extremo es rara. Las redes duplican. Los clientes reintentan. Los brokers re-entregan. Los timeouts causan resultados ambiguos.
Construye alrededor de una propiedad mas fuerte y mas alcanzable: procesamiento idempotente. Requiere un idempotency key para cada intencion del cliente. Almacenalo con una restriccion unica. Devuelve el resultado anterior en reintentos.
Si solo haces una cosa despues de leer este articulo, haz esto: pon una restriccion unica en idempotency_key, trata los duplicados como normales, y haz que tus handlers sean seguros de ejecutar dos veces. Prevendras una cantidad alarmante de incidentes de "doble debito."
VII. Rutas de lectura, proyecciones y conciliacion
Las escrituras suelen ser fuertemente consistentes. Las lecturas a menudo no lo son. Si lees de una replica, puedes ver datos obsoletos. Incluso un retraso pequeno puede violar las expectativas del usuario. La documentacion de AWS senala que el retraso de replica puede variar con la carga de escritura.
Para UX de dinero, define una politica: para "saldo ahora mismo," lee del escritor. O usa controles de consistencia a nivel de sesion. O proporciona semantica de "a partir del timestamp." No mezcles silenciosamente. El peor bug es un bug que parece fraude.
Rendimiento sin mentiras
Un libro contable puro dice: calcula el saldo sumando movimientos. Correcto. Tambien costoso a escala. Asi que agregas proyecciones:
Capa 1 -- Libro contable inmutable. Los movimientos son solo de insercion. Son el sistema de registro.
Capa 2 -- Saldos en cache. Mantener account_balances como una tabla derivada. Actualizarla en la misma transaccion que los movimientos. Tratarla como cache.
Capa 3 -- Puntos de control. Almacenar instantaneas periodicas de saldos. Diario es comun. Esto acelera "saldo a partir del momento T."
Capa 4 -- Conciliacion. Recalcular saldos desde los movimientos. Comparar con saldos en cache. Alertar ante desviaciones. Reparar mediante replay.
La conciliacion no es opcional en sistemas serios. Es tu red de seguridad. Y es tu superpoder de depuracion. Las pruebas estilo Jepsen y el historial de incidentes reales muestran que las bases de datos pueden comportarse de formas sorprendentes bajo fallos. Tu libro contable mas la conciliacion es como detectas y te recuperas de esas sorpresas.
VIII. Seguridad: El modelo de amenazas no son solo los hackers
En pagos, la seguridad incluye atacantes externos, errores internos, abuso de insiders, accidentes operacionales, corrupcion de datos y procesos de disputa. Un libro contable ayuda porque es solo de insercion y explicable. Pero aun necesitas controles.
Minimo privilegio en todas partes. Usa roles de base de datos separados: un rol de contabilizacion que puede insertar movimientos y asientos de diario, un rol de lectura que puede consultar pero no mutar, y un rol de administrador que es raro y auditado.
Inmutabilidad por politica y por mecanismo. No permitas UPDATE o DELETE en movimientos en rutas normales. Usa permisos de base de datos para aplicarlo. Griffin prefiere explicitamente escribir un asiento de diario de reversion en lugar de reescribir el historial (Griffin, 2022). Ese es el enfoque contable correcto. Tambien es el enfoque de seguridad correcto.
Auditabilidad y manejo de disputas. Los pagos rapidos producen disputas. Necesitan procesos, plazos y reglas de compensacion. El RBI ha publicado marcos para tiempos de respuesta y compensacion al cliente por transacciones fallidas. El Banco Mundial ha discutido la resolucion de disputas en sistemas de pago rapido. Estas reglas empujan la arquitectura en una direccion: necesitas estados, trazabilidad y prueba de lo que sucedio. Un libro contable es tu sustrato de evidencia.
Seguridad de API y resistencia al abuso. Limita la tasa de consultas de estado. Estrangula la actualizacion de saldos. Haz los endpoints idempotentes. Rechaza tormentas de reintentos abusivos. Correccion y seguridad se superponen aqui. La mayoria de los fraudes comienzan como una condicion de carrera mas ingenieria social.
IX. El plano: Un servicio de pagos basado en libro contable
Aqui hay una arquitectura que puedes desplegar.
+-----------+
| API GW |
+-----+-----+
|
+-------v--------+
| Servicio de |
| pagos |
+-------+--------+
|
+----------------+----------------+
| |
+------v-------+ +-------v--------+
| BD del libro | | Flujos de |
| (PostgreSQL) | | liquidacion |
| | | |
| accounts | | INSTRUIDO |
| journal_ent |<--------------->| RETENIDO |
| postings | | COMPLETADO |
| outbox_evts | | FALLIDO |
| acct_balances| +----------------+
+--------------+
|
+------v-------+
| Lector de |---> Broker de mensajes ---> Consumidores
| outbox | (idempotentes)
+--------------+
Ruta de escritura: transferencia interna. Validar entrada. Iniciar transaccion. Bloquear cuentas en orden deterministico. Insertar asiento de diario con idempotency key. Insertar movimientos balanceados. Actualizar saldos en cache. Insertar evento outbox. Marcar asiento como contabilizado. Commit.
Ruta de escritura: pago externo. Crear fila de flujo de trabajo. Reservar fondos (retencion). Llamar al riel de pago externo. Al confirmar, contabilizar asiento de diario final. Al fallar, liberar retencion. Siempre conciliar asincronamente.
Estrategia de concurrencia. Elige una: locks pesimistas por cuenta para comportamiento predecible, o serializable + reintentos si tu plataforma lo soporta y tu codigo es seguro para reintentos. El diseno SSI de PostgreSQL esta bien documentado. Las bases de datos distribuidas como Spanner proporcionan propiedades aun mas fuertes, pero con complejidad de sistema significativa. No elijas por ideologia. Elige por realidad operacional.
Por que los libros contables perduran
El enfoque de libro contable gana porque te da propiedades que no puedes agregar despues.
Explicabilidad. Cada cambio de saldo tiene una razon. Puedes responder "por que cambio esto" desde los movimientos.
Recuperabilidad. Desplegaste un bug? Replay. Perdiste un cache? Reconstruye.
Concurrencia controlada. Bloquea lo correcto. Aisla las invariantes.
Reversiones seguras. Reversa por compensacion, no por reescritura. Eso es exactamente lo que los auditores quieren.
Integracion con flujos de trabajo. Retenciones, estados de liquidacion, disputas y contracargos se convierten en capas aditivas. No contaminan la verdad del libro contable.
ACID te da un limite de commit correcto. No te da dinero correcto. La seguridad del dinero viene de un libro contable de partida doble que conserva valor, control de concurrencia cuidadosamente delimitado, idempotencia como regla de primera clase, estados de flujo de trabajo para incertidumbre externa, publicacion de eventos basada en outbox, conciliacion como operacion normal, y controles de seguridad que asumen errores y abuso.
Este es el secreto silencioso del software financiero. Se trata menos de algoritmos heroicos. Se trata mas de invariantes disciplinadas. Es ingenieria que respeta la regla favorita del universo: las cosas fallan.
Y por eso los libros contables perduran. No porque sean viejos. Porque son dificiles de engañar.
Referencias
- Berenson, H. et al. (1995). A Critique of ANSI SQL Isolation Levels. ACM SIGMOD. PDF
- PostgreSQL Documentation. Transaction Isolation. postgresql.org
- Ports, D. & Grittner, K. (2012). Serializable Snapshot Isolation in PostgreSQL. VLDB. PDF
- Griffin Bank. (2022). Building an Immutable Bank. griffin.com
- TigerBeetle. Financial Transactions Database. tigerbeetle.com
- Richardson, C. Saga Pattern. Microservices.io. microservices.io
- Richardson, C. Transactional Outbox. Microservices.io. microservices.io
- Confluent. Why Dual Writes Are a Bad Idea. confluent.io
- Reserve Bank of India. Turn Around Time and Customer Compensation. rbi.org.in
- World Bank. Fast Payment Systems. worldbank.org
- Kingsbury, K. Jepsen: Distributed Systems Safety Research. jepsen.io
- AWS. Working with Read Replicas. docs.aws.amazon.com