Manual de ingeniería fintech
(w.pitula.me)- Los sistemas que tratan el dinero como estado central deben diseñarse sobre los principios de no crear datos, no perderlos y no confiar en nada
- La representación de importes debe evitar
floaty combinarBigDecimal, enteros en la unidad mínima, números racionales, etc., según la responsabilidad; la serialización de números en JSON también puede recrear el problema de IEEE-754 double - El libro contable debe mantener saldos e informes reconstruibles mediante contabilidad de partida doble, trazas de auditoría inmutables, separación de value time, booking time y settlement time, y registros de correcciones y cancelaciones
- El flujo real de dinero debe evitar gastos duplicados y omisiones mediante reservas, idempotencia, máquinas de estado reiniciables, validación de API externas y webhooks, outbox y CDC, y conciliación (reconciliation)
- El control de acceso, la aprobación four-eyes, el seguimiento de cambios en el SDLC, las pruebas basadas en propiedades y la inyección de fallas hacen que incluso los operadores internos y los cambios de código se traten como límites de confianza
Principios básicos de los sistemas fintech
- En la ingeniería de software donde el dinero es la principal preocupación del sistema, la trazabilidad, la inmutabilidad y la verificabilidad son mucho más importantes que el CRUD general
- El público objetivo son personas que acaban de sumarse a fintech, quienes ya trabajan en fintech y quienes, desde fuera de fintech, quieren entender en qué se diferencian los sistemas de dinero de los sistemas comunes
- Todos los patrones son medios para respetar tres principios
- No invented data: como el dinero no puede crearse de la nada, no se permite el procesamiento duplicado ni cambios arbitrarios de saldo
- No lost data: todo lo que le ocurre al dinero debe ser rastreado y persistido
- No trust: se verifica sin confiar en proveedores externos, componentes internos ni el mundo real
Formas de representar el dinero
- La representación de importes es la decisión más básica de un sistema financiero; si se elige mal, todas las capas superiores heredan errores
- float/double casi nunca es una buena opción, porque puede producir pérdidas de precisión difíciles de predecir
- Tiene la ventaja de ser rápido, eficiente en memoria y no requerir bibliotecas ni estructuras de datos adicionales
- Los tipos de precisión arbitraria como
BigDecimalpermiten controlar explícitamente la precisión del cálculo y el punto de redondeo- Son adecuados para cálculos intermedios con varias operaciones encadenadas, como FX o cálculo de precios
- Guardar como enteros en la unidad mínima es el enfoque que usa precisión fija, como los sistemas de bancos centrales, para la mayoría de las monedas fiduciarias
- €12.34 se guarda como
1234 - Deben seguirse los dígitos de ISO 4217 y no asumir que siempre son 2 decimales
- Las criptomonedas también usan enteros en unidades mínimas como satoshi o wei, pero la precisión varía según el activo y la define el token, como
decimalsen ERC-20 - Los importes de criptomonedas pueden superar los enteros de 64 bits, por lo que pueden requerir enteros de ancho arbitrario
- €12.34 se guarda como
- Los números racionales son lo más potente cuando no se permite ninguna pérdida de precisión, pero son lentos, es difícil convertirlos a otros formatos sin pérdida de precisión y por lo general requieren tipos personalizados o bibliotecas
- La forma de almacenamiento y la forma de cálculo son decisiones separadas; un mismo sistema puede usar almacenamiento en enteros y cálculos intermedios con
BigDecimal - El manejo de límites también es importante en la serialización de importes
- Los números JSON comunes son IEEE-754 double en la mayoría de los parsers, así que aunque se cuide la representación interna, el problema de float reaparece en los límites
- El dinero debe enviarse como strings como
"12.34"o como enteros en la unidad mínima
Redondeo y manejo de monedas
- El redondeo es inevitable en divisiones, conversiones de moneda, comisiones, intereses, aplicación de porcentajes y cambios de precisión, por lo que no debe dejarse implícito
- La estrategia de redondeo es una decisión de negocio
- Hay casos en los que debe redondearse de forma conservadora, y también puede usarse half-even por sus efectos estadísticos
- Quién se queda con la fracción residual puede tener efectos legales y fiscales
- Conviene mantener la precisión completa el mayor tiempo posible y redondear solo en límites, normalmente antes de guardar o antes de mostrar al usuario
- Si se divide un valor en varias partes y luego se redondea, la suma de las partes puede diferir del valor original
- Según el caso, puede requerirse una cuenta de redondeo explícita
- El dinero no puede representarse solo con un número; siempre debe manejarse junto con la moneda
- Agrupar importe y moneda en un newtype, struct, class o record como
Moneyreduce la posibilidad de errores - Debe prohibirse la suma de monedas distintas, y las conversiones deben realizarse explícitamente con tipos de cambio estrictamente controlados
- No se deben aceptar códigos de moneda arbitrarios; en los límites del sistema deben validarse contra un conjunto controlado de monedas
- Los códigos de monedas fiduciarias pueden usarse como identificadores, pero las criptomonedas requieren una identificación más compleja, como
(network, contract address) - Las criptomonedas pegged, bridged o wrapped no son equivalentes al activo subyacente
- Agrupar importe y moneda en un newtype, struct, class o record como
Tipos de cambio FX
- Un FX rate siempre tiene direccionalidad
- El tipo EUR/USD no es simplemente el inverso de USD/EUR
- En un exchange, comprar y vender son órdenes con precios distintos por el bid/ask spread
- El momento del tipo de cambio también cambia el resultado
- El tipo de cambio actual se usa para calcular el valor de posiciones actuales o de transacciones asumidas como ocurridas ahora
- El tipo con value-date se usa para calcular cambios de valor o impuestos
- En una conversión importan dos tipos de cambio
- El Transactional rate es el tipo al que ocurrió la conversión real y se deriva del importe original y el importe resultante
- El Reference rate se usa para valuaciones y juicios de equivalencia, como valor de posiciones o bases fiscales, y no es un precio de operación real
- No existe un tipo de cambio único estándar
- Los tipos de cambio vienen del mercado y varían según el lugar de negociación o el método de cálculo
- Los tipos de cambio de bancos centrales son lo más cercano a un estándar, pero solo pueden usarse como reference rate, y las fuentes alternativas también pueden ser válidas
- Debe guardarse la fuente del importe junto con la del reference rate para poder verificarlo más adelante
Libro contable y partida doble
- Los movimientos de dinero deben registrarse de forma auditable y reconstruible incluso años después
- La contabilidad de partida doble es un enfoque ampliamente usado que guarda las transacciones financieras como una lista de entries con la forma
(credit account, debit account, amount)- La representación clásica tiene una fila de débito y una fila de crédito separadas para cada movimiento
- Como cada entry mueve el mismo importe de una cuenta a otra, el libro contable siempre queda balanceado
- El dinero siempre tiene un origen y un destino
- Los proveedores externos también deben tener cuentas dedicadas para poder rastrear el dinero que entra y sale del sistema
- Los saldos no se guardan; se derivan de los movimientos de dinero
- Las cuentas tienen tipos como asset, liability o equity
- Se mantiene la accounting equation
assets = liabilities + equity - En la práctica también se necesitan cuentas de revenue y expense para registrar ingresos por comisiones o pérdidas por write-off
- La fórmula extendida es
assets = liabilities + equity + revenue - expenses
- Se mantiene la accounting equation
- Una sola transacción suele generar varios movimientos
- Puede haber un movimiento por el importe neto y otro por la comisión
- Por convención, un posted entry es inmutable, y las correcciones se manejan agregando un nuevo entry que compensa el original
Modelo de tiempo: value, booking, settlement
- Una transacción suele tener dos o más timestamps, a veces tres
- Value time: momento en que la transacción ocurrió realmente
- Booking time: momento en que se registró en el sistema
- Settlement time: momento en que el dinero se transfirió o se materializó realmente
- El settlement time no existe en todas las transacciones y normalmente se expresa como T+X
- T+2 significa que el settlement ocurre 2 días después del value date
- Value time y booking time casi siempre difieren
- Si booking > value, es backdated, lo que es especialmente importante cuando cambia el período de reporte
- Si booking < value, es forward-dated y ocurre en pagos programados o pagos con fecha futura
- En un ejemplo de pago con tarjeta, el pago ocurre en T1, el sistema lo registra en T2 y el proveedor de pagos transfiere el dinero a la cuenta en T3
- Los reportes de negocio suelen mirar principalmente el value time o el settlement time, mientras que el booking time es útil para la trazabilidad
- Si se combinan varios momentos en un único
created_at, se pierde información que no podrá reconstruirse más adelante
Pista de auditoría, event sourcing e inmutabilidad
- Los sistemas financieros están sujetos a auditorías regulatorias y pueden tener que demostrar si se mezclan fondos de usuarios y fondos de la empresa, la explicabilidad de los ingresos, si la información entregada a terceros coincide con la realidad, el estado de protección de los fondos, etc.
- Un audit trail es el historial completo no solo del estado actual, sino también de cómo se llegó a ese estado.
- Qué ocurrió
- Cuándo ocurrió
- Quién o qué lo disparó
- Por qué ocurrió
- No solo los movimientos de dinero requieren una pista de auditoría, sino también las intervenciones manuales, los cambios de configuración como fee schedule, rate source y limit, y los cambios de permisos.
- En decisiones como un compliance check o un risk score, guardar solo el resultado puede ser insuficiente.
- Si están en una decision table o un rules engine como DMN, Drools o Decisions4s, se obtiene una estructura reproducible que permite saber qué regla se ejecutó con qué entradas y qué resultado produjo.
- Event sourcing es un enfoque sistemático para crear un audit trail.
- En lugar de guardar por separado el estado actual y el log, se guardan solo eventos y se deriva el estado a partir de ellos.
- Un ledger de partida doble es un ejemplo de este patrón, en el sentido de que no guarda el balance, sino que lo calcula a partir de las entries.
- Event sourcing también tiene grandes restricciones prácticas.
- No se necesita en todas partes; si el ledger ya cubre el dinero, para los dominios periféricos puede bastar un modelo normal y un change log confiable.
- Por rendimiento, se pueden cachear o hacer snapshots de balances y projections.
- La fuente de eventos puede ser difícil de consultar eficientemente, por lo que puede aumentar el trabajo con projections.
- Como los eventos permanecen durante años, el código de hoy también debe poder leer eventos de hace mucho tiempo.
- Una pista de auditoría debe ser append-only, porque si puede modificarse no sirve como evidencia.
- Herramientas para esto incluyen tablas append-only, quitar
UPDATEyDELETEde los permisos de la DB, bloquear mutating operations en la capa de aplicación y usar checksums o hash chains como tamper evidence.
- Herramientas para esto incluyen tablas append-only, quitar
- En sistemas reales, a veces hay que corregir un event log o un audit trail por un bug.
- Por lo general, una vez que los datos se reportaron al exterior deben quedar fijos; si aún no se reportaron, puede ser posible corregirlos in situ antes de que salgan del sistema.
Cancelaciones y correcciones
- Pueden ocurrir errores como un posting con monto incorrecto o un posting a la cuenta equivocada.
- La inmutabilidad exige una forma de corregir hacia adelante.
- Se postea una nueva compensating entry y se la vincula bidireccionalmente con el record original.
- Un Reversal compensa por completo el original como si económicamente no hubiera existido, pero tanto el original como el reversal quedan en el historial.
- Una Correction o adjustment consiste en bookear la diferencia entre el registro real y el valor correcto, o revertir y luego volver a postear con el valor correcto.
- Una corrección puede entrar en un período de reporte distinto al del original.
- Debe haber información de vinculación para que los reportes puedan atribuir correctamente la corrección y distinguir la actividad real del cleanup.
- Como normalmente no se permite backdate en un período de reporte ya cerrado, si se especifica un value time pasado al hacer una corrección depende del calendario de reportes.
Ejecución de flujos de dinero: invariantes y reserva de fondos
- Un Invariant es una propiedad que siempre debe ser verdadera en el sistema.
- La accounting equation es un ejemplo, y los stakeholders del negocio pueden definir varias condiciones.
- Las formas de imponer invariantes son complementarias.
- Diseñar para crear solo objetos válidos en la etapa de creación.
- Verificar en runtime con assertions, pruebas y property-based testing.
- Analizar posteriormente los datos almacenados con reconciliation jobs o nightly checks.
- Las transacciones que interactúan con el mundo externo deben evitar race conditions.
- Hay que evitar situaciones en las que recién después de una llamada externa se descubre falta de saldo, o en las que se gasta el mismo dinero dos veces.
- Funds reservation o hold-and-release es un patrón que reserva fondos para una transacción específica antes de interactuar con el exterior.
- Si tiene éxito, se hace settle de la reservation y la transacción continúa.
- Si falla, se hace release y vuelve al available balance.
- Este patrón distingue entre total balance y available balance.
available = total - reserved- La verificación de saldo y la nueva reservation se realizan con base en el available balance.
- El monto final puede diferir de la estimación previa.
- Si cambian las comisiones o el tipo de cambio, se reserva el monto estimado, se hace settle del monto real y se libera el resto.
- Toda reservation debe terminar necesariamente en settle o release.
- Una reservation huérfana bloquea fondos del usuario, pero no pierde ni crea dinero.
- Un expiry o timeout puede servir como red de seguridad, aunque no es indispensable.
- La verificación de saldo y el registro de la reservation deben ser linearizable.
- Con un stale read, dos transacciones pueden pasar ambas la verificación y quedar respaldadas por los mismos fondos.
Overdraft e idempotencia
- Overdraft es la situación en la que el saldo de una cuenta se vuelve negativo.
- Un overdraft intencional es un producto de crédito con límite e intereses, y normalmente se modela como una cuenta de overdraft separada.
- Un overdraft no intencional puede ocurrir aunque esté prohibido por política.
- El settlement puede llegar por un monto mayor que la estimación reservada, o un reversal puede llegar después de que los fondos ya salieron.
- Funds reservation reduce la ventana, pero no la elimina.
- “Prohibido” y “no representable” son cosas distintas.
- Si se impide representar saldos negativos usando un unsigned integer o
CHECK (balance >= 0), cuando en la realidad haya que aceptar un saldo negativo eso puede terminar en crashes, zero clamp o procesamiento incorrecto. - Hacer clamp de un saldo negativo a 0 crea dinero.
- Si se impide representar saldos negativos usando un unsigned integer o
- Cuando se detecta un overdraft, debe tratarse como una señal de investigación y recuperarse o manejarse explícitamente mediante future deposits y netting, una solicitud de repayment, o un write-off contra una cuenta de expense/loss.
- En sistemas distribuidos no se puede garantizar exactly-once delivery, por lo que se necesitan reintentos, y los reintentos pueden generar entregas duplicadas.
- Idempotency es la propiedad de que, aunque el mismo mensaje se entregue dos veces, el procesamiento tenga efecto una sola vez.
- Una idempotency key explícita suele ser más simple y segura que la deduplication basada en payload.
- La key debe limitarse al alcance de una operation y de un client específicos.
- Hay que decidir si reproducir el error o reprocesar; para los errores permanentes, normalmente es más simple reproducirlos tal cual.
- Verificar que el payload de una llamada duplicada sea igual al original es una buena práctica, pero tiene costos de complejidad de implementación y flexibilidad.
- A gran escala, se necesita deduplication de miles de millones de requests y una atomic barrier ante accesos concurrentes.
- Una idempotency window de 24 horas simplifica la implementación, pero tiene un alto costo de correctness.
- También se necesitan pruebas de retry y manejo de out-of-order retry.
Flujos reanudables
- Hay que asumir que los flujos de dinero abarcan varias etapas y pueden morir en cualquier punto entre etapas.
- Full resumability es un diseño que garantiza que un flujo a medio terminar siempre quede en un estado recuperable.
- El estado de avance debe guardarse en almacenamiento persistente, no en memoria.
- Modelar el flujo como una state machine explícita y hacer commit de la finalización de cada etapa antes de iniciar la siguiente.
- Se necesita un driver independiente que vuelva a empujar los flujos interrumpidos.
- Un scheduler, worker o poller debe procesar los incomplete flows incluso después de un crash del orchestrator.
- Al reanudar, puede ser necesario volver a ejecutar etapas que ya ocurrieron parcialmente, por lo que cada etapa debe ser idempotente.
- Los efectos externos no pueden hacerse rollback.
- Después de llamar al mundo externo, no se puede volver al estado de no haber llamado.
- Hay que hacer roll forward hasta completar, o si una etapa posterior falla permanentemente, postear una compensating action como en el saga pattern.
- Se puede usar un durable-execution engine como Temporal, Camunda, Workflows4s o AWS Step Functions, o construir una persistent state machine propia.
Consumo de API externas
- Las API externas, como proveedores de pago, custodians, nodos de blockchain o vendors de KYC, deben tratarse de forma defensiva porque no se puede controlar su código, calidad ni tiempo de actividad.
- No se debe confiar en el esquema.
- Pueden faltar campos, cambiar tipos o aparecer valores
nullinesperados. - Las partes importantes deben validarse en el límite, y los datos inesperados deben fallar de forma evidente.
- Si se validan incluso partes innecesarias, una violación de contrato por parte de un tercero puede causar incidentes innecesarios.
- Pueden faltar campos, cambiar tipos o aparecer valores
- En API externas pueden ocurrir perfectamente cosas como pasar tokens en la URL, pérdida de precisión, códigos HTTP que no coinciden con el significado, cuerpos de error dentro de un
200, paginación inconsistente o formatos de fecha personalizados. - Todas las llamadas pueden fallar, por lo que se necesitan timeout y retry.
- Un circuit breaker suele ser principalmente una cortesía hacia servidores sobrecargados y aumenta la complejidad del cliente.
- Sin embargo, puede ser necesario para proteger recursos finitos como latencia, threads y conexiones.
- Para rate limits y quotas, hay que calcular de antemano el volumen esperado de llamadas y compararlo con los límites del provider.
- Guardar todos los requests y responses en una forma estructurada y consultable sirve como material para investigaciones, audit trail, evidencia en disputas sobre el comportamiento del provider y reprocesamiento después de bugs.
- En áreas críticas se puede considerar redundancia de providers.
- Hay enfoques como validar datos con dos nodos de blockchain, tener un partner bancario de respaldo, un crypto custodian o un vendor de KYC.
- Los costos de desarrollo, comisiones y complejidad son muy altos.
- El sandbox puede diferir mucho de production, por lo que hay que preparar pruebas en production mediante canary releases o controlled usage de bajo impacto.
Procesamiento de webhooks
- Los webhooks son una forma común de recibir señales de sistemas externos, pero procesarlos de manera segura no es fácil.
- No se debe asumir el orden.
- Los mensajes pueden llegar out-of-order o contener stale data.
- No se debe considerar que el webhook recién recibido es la verdad más reciente y sobrescribir el estado.
- No se debe asumir la validez.
- Los webhooks vienen de otros subsistemas del issuer y pueden contener datos stale o transformados incorrectamente.
- Conviene usar el cuerpo del webhook solo como trigger y consultar la API para confirmar el authoritative state.
- La API también puede ser eventually consistent, por lo que si se consulta de inmediato puede devolver un estado anterior; se necesita retry.
- No se debe asumir la entrega.
- Aunque el issuer prometa una política fuerte de redelivery, algún día un webhook se perderá.
- Un proceso independiente como reconciliation debe complementar la integridad de los datos.
- Tampoco se debe asumir una única entrega.
- El mismo webhook se entregará varias veces y el procesamiento debe ser idempotente.
- Hay que acknowledge rápidamente y procesar de forma asíncrona.
- Se guarda el raw event en un durable store, se devuelve inmediatamente un 2xx y el trabajo real se ejecuta de forma asíncrona.
- El raw payload debe guardarse tal como llegó.
- Es necesario para procesamiento confiable, audit trail y reprocesamiento después de bugs.
- Se debe verificar al llamador.
- Por lo general, el issuer adjunta una payload signature, y el receptor la verifica con un HMAC de un shared secret o con una firma asimétrica basada en clave pública.
- La verificación de la firma debe hacerse sobre los raw bytes recibidos, no sobre un payload serializado nuevamente.
- Los webhooks deben tratarse como un hint de que algo ocurrió, no como la verdad sobre qué ocurrió.
Notificaciones confiables: Outbox y CDC
- Cuando hay que notificar de forma confiable cambios del sistema a canales externos, como Kafka events o webhook calls, la transactionalidad se vuelve un problema.
- Pueden darse situaciones en las que el publish tuvo éxito pero, por un problema de red, no se recibió la respuesta y se hace rollback del estado del sistema, o en las que el state change se commiteó pero el publish falló.
- La respuesta de libro de texto es 2-phase commit o distributed transaction, pero se usa rara vez por su complejidad y la dificultad de estandarizar su reutilización.
- Hay varias opciones prácticas.
- Outbox pattern: registrar transaccionalmente el publishing intent en un store dedicado junto con el cambio de estado, y procesarlo luego hasta que tenga éxito.
- Change Data Capture: leer el write-ahead log o replication log de la base de datos y convertir los cambios commiteados en un event stream.
- Debezium y AWS DMS ofrecen CDC.
- CDC emite raw events con forma de filas de tabla, por lo que se requiere postprocessing para evitar filtrar el schema interno.
- Listen-to-yourself primero publica un event y luego reconstruye su propio estado a partir de ese event.
- En event sourcing, el event log ya está en la DB, así que se puede publicar desde ahí.
- Sea cual sea el mecanismo elegido, el delivery es at-least-once.
- Si un relay o connector crashea después de publicar y antes de registrar, puede volver a enviar al reiniciar.
- El consumer debe deduplicate con un stable event id y comportarse de forma idempotente.
Reconciliation
- Los sistemas que dependen de datos externos son vulnerables al data drift, donde los estados de dos sistemas se desalinean.
- Puede perderse un webhook, o una transaction puede estar posteada en el ledger pero no reflejarse en el sistema del external provider.
- Reconciliation es el proceso de alinear dos sistemas.
- En la práctica, pueden ser tres o más, como ledger, payment processor y banco.
- La cadence puede ser hourly, daily, monthly o yearly, según el contexto y las restricciones.
- El drift puede ser missing data o una diferencia más compleja, como montos distintos para la misma transaction.
- El timing también es importante.
- Si el settlement es T+3, el record puede permanecer unreconciled durante 3 días, por lo que esto debe reflejarse en el proceso para evitar alerts innecesarias.
- El matching algorithm es la dificultad central.
- Por lo general, guardar internamente el external provider id simplifica el matching.
- Si no existe, pueden necesitarse heurísticas basadas en amount y time.
- También se necesita one-to-many reconciliation.
- Una sola settlement transfer puede abarcar varias transactions.
- No se debe simplemente sobrescribir para que una discrepancy encaje con la reconciliation.
- Hay que entender y corregir la causa con soporte de primera clase, como correction records o reprocesamiento de webhook data.
Control y acceso
- Los sistemas de dinero deben controlar no solo los datos, sino también quién puede realizar qué acciones, y demostrar después que se cumplieron los procedimientos.
- Segregation of duties es un control que impide que una sola persona sea dueña de todo el proceso.
- Four-eyes, maker-checker y dual control son mecanismos en los que una segunda persona debe aprobar una action antes de que se aplique.
- Se aplican a acciones que mueven fondos o pueden mostrarlos incorrectamente, como withdrawals grandes o manuales, manual ledger corrections, movimientos de treasury y cold-wallet, o cambios en fee schedules o limits.
- Los mismos controles se aplican a la ingeniería.
- Code merge, production deploy e infrastructure change son actions sensibles en sistemas de dinero, por lo que requieren review y approval.
- El approval en sí también forma parte del trail.
- Hay que registrar quién lo solicitó, quién lo aprobó y si eran dos personas distintas para poder demostrar el control.
- Para emergencias, se necesita una ruta break-glass explícita y fuertemente auditada.
- Access control es parte del estado del sistema y cambia con el tiempo.
- Tanto humans como services deben recibir el mínimo privilegio.
- Preferir RBAC antes que grants por persona facilita la review.
- Los capability grants y revokes también son eventos sensibles, por lo que hay que registrar qué cambió, quién lo cambió y por qué.
- Las scheduled access reviews deben detectar permission drift antiguo o inexacto.
Seguimiento de cambios en el SDLC
- En un entorno regulado, se debe poder auditar el proceso por el cual el código llega a production
- El source control es el historial de cambios
- El commit history atribuye todos los cambios a un author y los conecta con el motivo mediante review y un linked ticket
- Debe protegerse con signed commits, protected branches y prohibición de hacer force-push sobre el shared history
- El review y el pipeline deben ser obligatorios
- Son importantes los required reviews, los status checks y prohibir push directo a la main branch
- El deployment debe ser trazable
- Debe ser posible reconstruir qué version está en ejecución y quién hizo el release y cuándo, para poder vincular un incident con el cambio que lo causó
Estrategia de pruebas
- En sistemas de dinero, el espacio de operation sequences es grande y las fallas interesantes surgen de combinaciones, por lo que las pruebas son especialmente importantes
- Property-based testing verifica properties que deben cumplirse para cualquier input, más que un output específico
- Encaja bien con invariants y money math
- Al generar operation sequences, se deben verificar los invariants no solo al final, sino después de cada paso
- Como es difícil hacerlo manualmente a gran escala, se necesita un testing harness que inyecte assertions automáticamente
- El generative idempotency testing verifica que toda operation que toca el mundo externo no tenga efecto en una segunda llamada
- La crash and resume injection verifica que un long flow pueda recuperarse aunque falle entre cualquier etapa
- El round-trip testing comprueba si, después de encode/decode, serialize/deserialize o convert/convert back, se vuelve al punto de partida o se queda dentro de una tolerance conocida
- Es una forma rápida de detectar pérdida de precisión en boundaries de money y currency types, y serialization bugs
- El golden testing compara resultados de cálculos como fee breakdowns, statements y reports contra resultados esperados guardados para revelar diffs no intencionales
- El backward-compatibility testing mantiene un corpus de payloads antiguos en formato real y verifica que el código actual todavía los deserialice y proyecte correctamente
- El production testing puede ser necesario cuando el sandbox difiere mucho de production
- Ejemplos: canary releases, controlled rollouts con blast radius pequeño y synthetic transactions que hacen fluir continuamente montos reales pequeños
- Como las production tests mueven dinero real, deben pasar por el mismo ledger, reconciliation y audit trail, y limpiarse mediante las rutas normales de correction/reversal
Términos del dominio y referencias
- En la introducción a fintech, el vocabulary y los concepts pueden ser más difíciles que el código, por eso se resumen por separado los términos clave
- El área de contabilidad y ledger incluye ledger, general ledger y sub-ledger, debit/credit, posting, chart of accounts, receivable/payable, IOU, accrual vs cash basis, trial balance, suspense/clearing account, write-off, commingling y reconciliation break
- El área de Money y FX incluye Money type, minor units, basis point, notional, fiat vs crypto, stablecoin, pegged/wrapped/bridged, bid/ask/spread, mid-market rate, reference rate y mark-to-market
- El área de transacciones y settlement incluye value date, booking date, settlement date, T+X, clearing vs settlement, cut-off time, float, netting, backdating y reversal/correction
- También se resumen por separado términos de pagos, tarjetas, mercados, crypto y compliance
- PSP, omnibus account, FBO account, chargeback, issuer/acquirer, authorization vs capture
- order book, market vs limit order, maker/taker, slippage, liquidity, derivative, futures, perpetual, liquidation
- custody, hot/cold wallet, private key, multisig, MPC, gas, confirmation/finality, reorg, UTXO vs account model
- KYC, AML/CFT, sanctions screening, PEP, SoF/SoW, Travel Rule, VASP, MiCA, least privilege, RBAC, audit trail
- Las referencias se dividen en contabilidad y ledger, payments y cards, markets y trading, crypto, engineering, KYC y AML
- Accounting for Computer Scientists: artículo para ingenieros que explica la contabilidad de doble entrada como grafo y modelo de datos
- Modern Treasury, How to Scale a Ledger: serie de artículos que aborda un production ledger desde la perspectiva de la ingeniería de software
- Designing Data-Intensive Applications: trata idempotency, logs, consistency y failure modes desde la perspectiva de sistemas
Tres ejemplos end-to-end
-
Retiro de criptomonedas
- Flujo en el que el usuario retira 0.5 ETH a una dirección externa
- La solicitud incluye una idempotency key para que los envíos duplicados creen un solo withdrawal
- Se reservan 0.5 ETH y la network fee estimada del available balance
- El compliance gate verifica sanctions, AML y la destination address, y puede quedar en sleep durante días por llamadas externas y revisión manual
- El on-chain broadcast debe ser idempotent y, después de un crash, se debe volver a verificar la chain en lugar de hacer un segundo broadcast
- Tras suficientes confirmations, se registran en el ledger los postings de user account debit, external on-chain account credit, network fee expense y service fee revenue
- Un nightly job reconcilia el ledger con la realidad de la chain
-
Depósito con tarjeta
- Flujo de recarga con tarjeta mediante un PSP
- El usuario envía el monto y la información de la tarjeta, y se abre una deposit transaction en el PSP con una idempotency key
- La authorization solo crea un hold; como el dinero aún no pertenece a la empresa, no se acredita el user balance
- El webhook
capturedverifica la firma sobre los raw bytes, guarda el raw payload y se procesa de forma asíncrona después de responder rápidamente con 2xx - Como el webhook es solo un trigger, se consulta el authoritative state desde la API del PSP
- El estado captured but not settled se registra mediante una clearing account, y el settlement puede llegar en batch después de T+X
- Un chargeback se maneja con una linked compensating entry, sin modificar el original
-
Conversión in-app con cashback
- Flujo en el que se cambian 1,000 EUR a USDC y se otorga cashback promocional
- La cotización EUR→USDC no es el inverso de USDC→EUR, sino una directional rate
- EUR y USDC no se suman entre sí; USDC se identifica por
(network, contract address)y no es lo mismo que una fiat pegged - Los cálculos mantienen la precisión completa y se redondea una sola vez en el límite con una estrategia explícita
- El spread debe registrarse explícitamente en una revenue account y no debe desaparecer como rounding residual
- El cashback no es un aumento gratuito de balance, sino dinero real que se mueve desde una company promotional/expense account al user balance
- La publicación del resultado garantiza reliable delivery mediante mecanismos como outbox, CDC o event log, y los downstream consumers hacen dedupe con un stable event id
1 comentarios
Opiniones en Hacker News
Lo hojeé, y me parece que este handbook es superficial y, en algunas áreas, roza el mal consejo.
Por ejemplo, si veo que los montos se almacenan en una forma que no es entera, saldría corriendo gritando. Por cosas como cuando el decimal de Rust se representa como punto flotante en JSON. Salvo que haya una razón muy fuerte, siempre deberían ser enteros, y la vista que se exporta puede tener el formato raro de codificación de bits que sea.
El tipo de cambio tampoco es un problema que se resuelva con un único punto en el tiempo. Influyen cosas como el tipo de cambio en el momento según el comprador, el tipo de cambio en el momento según el vendedor, el acuerdo, la tolerancia del acuerdo y el timestamp definitivo acordado.
Por la inmutabilidad, dan ganas de tener event sourcing en todos lados donde se maneja dinero. El stream final depurado se ve como
A -> B -> E, pero el stream real podría serA0 -> Edit(A0, A) -> B -> C -> D -> Rollback(B) -> E.Al final, no todas las Fintech son iguales. En algunas, el dinero se trataba como si fuera carga cualquiera; en otras, el dinero era el centro de todo.
Sobre divisas, también parece reforzar lo que dice el handbook: “no existe una tasa canónica”. Además, el texto trata sobre registros posteriores a la confirmación, y me parece que lo que mencionas habla de cómo confirmar. Es un matiz válido para un objetivo distinto, pero no parece prueba de que falte algo o esté mal.
En la parte de inmutabilidad también parece que el artículo dice lo mismo. No entiendo cuál sería la diferencia.
Si estás calculando precios de opciones con Monte Carlo sobre trayectorias de tasas de interés, y te importan métricas de riesgo como duración, convexidad o vega, a nadie le importa cuáles son las reglas de redondeo. Con double alcanza. ¿Cómo vas a forzar a enteros
exp(-rt)cashflowo la función de distribución acumulada normal?Hay áreas donde los enteros sí son lo correcto. Pero no es un principio universal; hay que elegir la opción de ingeniería adecuada.
Si el entorno que usas lo soporta, también puedes usar punto fijo, pero técnicamente sigue siendo un entero.
Para visualización, es seguro devolver valores decimal.
Qué bueno que salgas corriendo de un sistema que almacena montos como enteros. Así probablemente no terminemos trabajando en el mismo sistema. Hoy en día, más bien muchas veces me dan ganas de salir corriendo de sistemas que manejan montos como enteros. En una base de código ideal tocada solo por programadores financieros con mucha experiencia, puede funcionar bien, pero esos sistemas suelen correr el riesgo de volverse demasiado excluyentes o frágiles.
Como consejo para quienes estén considerando una estrategia de precisión de unidad menor para representar montos: mejor no hacerlo. Al menos no debería usarse como formato de datos de intercambio/API.
Parece ingenioso por cosas como operaciones enteras rápidas y ausencia de problemas de redondeo en sumas y restas, pero en cuanto trabajas con un socio que asume implícitamente una cantidad distinta de dígitos para una moneda determinada, te puedes meter en un problema enorme. Esto es especialmente importante con las stablecoins, que muchas veces tienen una cantidad implícita de decimales distinta de la moneda fiat que representan.
En APIs basadas en JSON, también vale la pena considerar representar los montos como strings. JSON no especifica precisión decimal, así que siempre tienes que verificar que tú y todos tus usuarios/proveedores no pierdan precisión porque el parser/serializador pase internamente por punto flotante. Puede ensuciarse rápido, y conceptualmente los strings se ven menos limpios, pero evitan por completo este problema. Algunos lo llamarían un antipatrón [1], pero no quisiera pelear esta batalla por pureza ideológica sobre los hombros de usuarios o accionistas.
[1] https://blog.json-everything.net/posts/numbers-are-numbers-n...
En el ámbito del trading de alta frecuencia, si puedes fijar de antemano un exponente consistente para algún {slice}, puedes ahorrar espacio de transmisión. Por ejemplo, enviar solo la mantisa dentro de un alcance como producto/tamaño de tick/clase de activo/exchange/feed/servidor, y que el cliente use un exponente hardcodeado.
Pero incluso en ámbitos similares, muchas veces vale la pena enviar un exponente
uint32adicional en los datos transmitidos. Así puedes cambiarlo más adelante y no quedas atado por un diseño inicial tipo “por ahora solo necesitamos centavos”. Por ejemplo, de pronto podrías tener que soportar el precio de bitcoin con precisión completa. Los usuarios te lo van a agradecer si no tienen que coordinar un cambio incompatible cuando quieras ajustar el exponente fijo.El estándar de “cualquier” es irracional. Es un estándar inalcanzable que puede exigir presupuestos de ingeniería ilimitados sin valor demostrable real.
Está bien identificar la falta de estándares, hablar de cómo se comportan los parsers reales y discutir brechas y casos de uso no cubiertos. También está bien proponer que se necesita un estándar más razonable. Pero no es buena idea exigir que todos soporten “todas las posibilidades”, algo que nadie necesita realmente, cuyo significado es poco claro y que en la práctica tampoco se puede lograr.
float/double, aritmética de punto fijo en milésimas de unidad menor o unidades aún más pequeñas, decimales de precisión arbitraria, o un enfoque completamente distinto?Como programador, cuando veo a programadores de Fintech hablar desde experiencias y perspectivas tan distintas, me pregunto qué significa realmente programar bien.
Que xlii diga que no hay que almacenar montos en punto flotante es el típico problema de IEEE 754. Para el seguimiento financiero, lo correcto es usar logs inmutables o registros basados en eventos, pero no creo que haga falta convertir todos los servicios circundantes a event sourcing. Pienso que basta con aplicarlo a la lógica central, como libro mayor, liquidación, órdenes y ejecuciones. Al leer el texto de xlii, parece una técnica que solo se puede concretar cuando el modelado salió bien.
Lo de lxgr señala el problema de las unidades menores. Si un número JSON se parsea como punto flotante por el lenguaje o el parser, se puede perder precisión. Normalmente se envía el valor junto con un campo separado para la cantidad de decimales. Pero he oído que en trading de alta frecuencia ese overhead en sí mismo es demasiado caro, así que no lo hacen.
Lo de antonymoose conecta con lo que dicen muchos libros. Por eso este tipo de diseño es común en contextos de FX o APIs. También se siente como diseño de protocolos.
En conjunto, todos tienen razón dentro de su propio dominio. Me gustaría que alguien como xlii fuera programador senior, pero al mismo tiempo siento que yo no podría diseñar un sistema tan complejo. En ese sentido, lo que dice cada uno es válido, y me resulta interesante ver cómo las opiniones divergen según el dominio. Me pregunto si esto es la experiencia especializada.
Al ver estas cosas, uno puede inferir más o menos de qué experiencia viene un programador. A veces la programación no se siente como encontrar la respuesta correcta, sino como elegir una visión del mundo.
Siempre es interesante ver en HN cómo los programadores modelan su propio dominio. A veces hago clic en sus perfiles y agrego su conocimiento de dominio a mi wiki personal, pensando que tal vez algún día me sirva.
Bien. Este libro ya contiene mucha buena información que también se puede encontrar en otros lugares, pero el solo hecho de reunirla lo vuelve bastante práctico. Recomiendo mucho Designing Data-Intensive Applications de Kleppmann. La primera edición fue muy buena y hace poco salió la segunda.
Trabajé como CTO en FinTech y construí todo el stack de software desde cero, y las lecciones del libro son en general correctas. Digo “en general” porque, como siempre, en proyectos concretos hay que considerar mucho el “depende de la situación”. Por ejemplo, yo no usé event sourcing para evitar el problema de calcular todo el estado. Un audit trail estándar append-only puede ser suficiente.
No se puede garantizar la entrega exactamente una vez, pero sí se puede construir un procesamiento efectivamente una vez, y en la práctica eso es lo que quieres.
Lo de guardar todas las solicitudes y respuestas es absolutamente correcto. No solo al consumir una API, sino también cuando se recopila cualquier información del mundo externo; y, si es posible, también se deberían registrar todas las etapas intermedias de transformación dentro del límite. Una combinación de buckets con direccionamiento por contenido y tablas relacionales funciona bien.
Además, el texto no dice nada sobre linaje de datos. ¿Qué haces si un proveedor actualiza algún dato al mediodía y tienes que saberlo sí o sí? Debes poder explicar ese cambio y, a la vez, permitir que al reejecutar cálculos que usaron valores antiguos se obtenga el mismo resultado. No es un problema especialmente difícil de resolver, pero requiere pensarlo.
Decir “mal consejo” es una forma bastante diplomática de expresarlo. Sinceramente, parece que la mayor parte de este “manual” la escribió un LLM
Por ejemplo, en la sección sobre inmutabilidad aparece esta frase: “Separar la PII de los datos financieros permite respetar el derecho de eliminación sin perder el historial financiero que debe conservarse”
En las instituciones financieras, ambas cosas van juntas por razones obvias de KYC/AML
Si, antes de que venza el período correspondiente, borras de inmediato el nombre, la dirección, etc. de un cliente cuando lo solicita y dejas solo los datos financieros, toda la organización va a tener un día muy malo cuando una autoridad legítima llegue a pedir los datos para rastrear delitos
Quien quiera trabajar en Fintech no debería depender de un “manual” cualquiera escrito por una persona desconocida en una jurisdicción desconocida
Quien trabaje en Fintech debería trabajar únicamente siguiendo el manual, las guías, etc. internas de su empleador. Esos documentos habrán sido elaborados junto con los abogados y responsables de compliance de la empresa para cumplir con las leyes y requisitos de reporte de las jurisdicciones donde opera el empleador
A mi entender, en realidad recomienda separar la PII que eventualmente debe eliminarse de los datos que forman parte de la ecuación contable/invariantes y que, en la práctica, quieres conservar de forma permanente. Así, una vez pasado el período de retención aplicable, puedes eliminar lo primero
Es cierto que no hay que depender de un “manual” escrito por una persona desconocida en una jurisdicción desconocida. Pero tampoco hay que ignorar ciegamente las ideas y prácticas que presenta, ni dejar de mirar fuera de la propia organización. Lo ideal es verlo y luego contrastarlo con tus conocimientos y la normativa local para ajustarlo
En un mundo donde solo existieran organizaciones perfectas y sin errores, el enfoque de seguir únicamente las directrices internas del empleador parecería razonable. Pero ¿cómo se llega a ese nivel sin conversaciones como esta?
Creo que la mayor parte de esto aplica no solo a Fintech, sino a la ingeniería de software en general
Por ejemplo, las partes sobre reintentos, idempotencia, orden de eventos, etc. aplican a cualquier sistema que requiera cierto grado de corrección, aunque el dinero no esté involucrado directamente. He visto demasiados sistemas construidos bajo la suposición de que “siempre se puede reintentar”, pero para que un reintento sea posible, primero el fallo debe ser limpio, y los subsistemas deben ofrecer el nivel de idempotencia que uno cree que ofrecen. Estas cosas a menudo no se verifican realmente
Preferiría leer un texto que defendiera un enfoque más radical, como una base de datos por cuenta. Algo así, con trade-offs propios dentro de Fintech
El consejo que más quisiera darle a un ingeniero o fundador de Fintech es que se tome en serio el riesgo y el compliance desde el primer día
Los sistemas financieros se basan en la confianza. Si no puedes mitigar el riesgo de forma demostrable, pierdes la confianza y, al final, pierdes todo el negocio
No es cierto que haya que evitar los números de punto flotante. Trabajé 20 años en Fintech y en su mayoría usamos double. Excel también usa double, el frontend usará double, y todas las bases de datos soportan double. Las bibliotecas estándar saben parsear double, y JSON, aunque quizá no en teoría, en la práctica usa double. Muchos sistemas ERP también usan double
Al manejar moneda con double, la clave es tener presente que puede representar 15 dígitos de precisión total. Mientras tus números no usen más dígitos que eso, como
123456789.01o123.456789, puedes tener precisión decimal perfecta en cálculos financieros. Basta con redondear siempre el resultado dentro de esos 15 dígitos de precisión después de cada cálculo y antes de cada comparación. Así lo hace ExcelLa mayor ventaja de double es que tiene soporte amplio y permite mezclar distintas precisiones dentro de un sistema. Eso pasa cuando trabajas con finanzas internacionales o productos financieros avanzados. Algunas cuentas requieren precisión hasta milésimas, y otras deben redondearse a múltiplos de 0.25. Al final probablemente no uses aritmética básica, sino una biblioteca especializada de matemática contable, y esa biblioteca puede usar perfectamente punto flotante como backend
Una verificación de saldo con Plaid no garantiza que un débito ACH que estás por enviar vaya a tener éxito
No importa si el saldo es de un millón de dólares. Antes de que se procese el ACH, todo el dinero puede (a) salir por transferencia wire, (b) liquidarse por ACH de ayer —facturas, débitos automáticos, etc.— y cheques, o (c) gastarse con tarjeta de débito/ATM
Quizá sea mejor que no diga cómo sé que algunas Fintech no manejan esto
Solo la sección sobre claves de idempotencia ya vale la pena leerla. La mayoría de los desarrolladores aprende esa lección por las malas
Muchos de ellos son anteriores a que el conocimiento sobre idempotencia estuviera ampliamente difundido, así que a menudo se terminan concatenando varios campos que parecen globalmente únicos para forzar una clave de idempotencia. El problema es que nunca son completamente únicos. A veces se puede ver lo que ocurre detrás de la cortina; por ejemplo, cuando un banco no permite hacer una transferencia por el mismo monto, en la misma fecha, a la misma cuenta destinataria
He pasado mucho tiempo explicando cómo debería funcionar la idempotencia y por qué es importante. La mayoría de los equipos entiende la necesidad, pero muy pocos la habían pensado desde el principio
“Fintech” es un término muy amplio, y la mayor parte de lo que se llama Fintech en realidad es comunicación. Comunicación entre empresas, entre traders, entre sistemas, entre libros contables. No existe una forma “correcta” de programar que aplique a toda la industria. Al final, la forma correcta es la que entiende la contraparte con la que me comunico
Si la contraparte rastrea la moneda en centavos, rastrearla con mayor precisión genera discrepancias de redondeo. Y al revés, pasa lo mismo si yo manejo centavos y la contraparte trabaja con décimas de centavo. Todos los demás consejos de este documento deben verse de esa manera