Una reserva no es un booleano. Es un asiento.

Un cliente de negocio tenía el 97% de su saldo bloqueado. Cero operaciones abiertas.
No era un bug de saldo. Era un bug de definición.
Así estaba construido: cuando una orden usaba el wallet, el sistema creaba un registro de detalle de pago. Al cerrar la orden, la rutina de liberación buscaba ese registro para saber cuánto devolver. Ese campo colateral era la única evidencia de que había algo reservado.
Entró un flujo nuevo. Cumplía todo lo que le pedía el negocio: cobraba bien, completaba la orden, dejaba al cliente contento. Simplemente no creaba ese registro.
Resultado: las órdenes terminaban perfecto y la liberación no encontraba nada que liberar. Semanas de bloqueos, ninguna devolución. El saldo se fue congelando de a poco, sin un solo error en los logs.
Si "cuánto está reservado" se deduce mirando tablas laterales, cada flujo nuevo que alguien escriba es una oportunidad de romper el cálculo en silencio. El que escribe el flujo nuevo ni siquiera sabe que existe la regla.

Con doble entrada la reserva deja de ser una inferencia:
- Bloquear = un asiento que mueve saldo de "disponible" a "retenido". Dos posteos, suma cero.
- Liberar = otro asiento, en sentido contrario.
- "¿Cuánto está retenido?" = un balance, no un JOIN esperanzado.
Y lo más importante: si un flujo nuevo bloquea y nunca libera, el dinero sigue ahí, en una cuenta con nombre, con su asiento y su fecha. No desaparece dentro de una condición que nadie recuerda.
El guard que escribimos después para detectar reservas atascadas tenía el mismo defecto de origen: hacía JOIN contra la tabla de reservas. Los bloqueos sin fila de reserva le eran invisibles. Un detector construido sobre la misma suposición que causó el problema no detecta nada.
Si tu sistema retiene dinero de terceros, la pregunta no es "¿tenemos bloqueos?". Es: ¿puedo listar cada retención activa con su asiento, sin mirar una tabla operacional?
Si la respuesta necesita un JOIN, ya sabes dónde va a aparecer el próximo saldo congelado.
(LedgerCore, lo que estoy construyendo, trata holds y releases como asientos de primera clase por exactamente esta razón.)
Comentarios
Todavía no hay comentarios. Empieza tú.