Volver al blog
• 8 min de lectura

BigDecimal no siempre es la decisión correcta

La regla 'usa BigDecimal para dinero y finanzas' está tan arraigada que dejamos de cuestionarla. Un issue abierto en una librería de finanzas cuantitativas me obligó a replantearme el dogma.

Java Arquitectura Backend Performance Finanzas
BigDecimal no siempre es la decisión correcta
Foto por Maxim Hopman

Hace unas semanas estaba navegando GitHub cuando me topé con quant4j: una librería Java para finanzas cuantitativas. Cubre pricing de bonos, duración, bootstrapping de curvas de tasas, solvers numéricos para YTM. Bien estructurada, bien testeada, con una API limpia orientada a interfaces.

Empecé a explorar el código con genuina curiosidad. Hasta que vi esto:

double price = face * Math.exp(-rate * time);

double. En una librería de finanzas.

Mi primer instinto fue automático: esto está mal. Cualquier desarrollador Java con experiencia en sistemas financieros tiene grabada a fuego la advertencia: nunca uses float o double para dinero. Y yo no fui la excepción.

Abrí un issue preguntando si considerarían soporte para BigDecimal, o al menos documentar por qué usaban double. Educado, pero con la premisa implícita de que era un descuido.

La respuesta del autor, Achraf Hasbi, me hizo detenerme.


Lo que me respondió el desarrollador

Achraf no se puso a la defensiva. Explicó con precisión por qué double era una decisión intencional:

“When I initially designed the library, I had to make a trade-off between performance and decimal precision. Quantitative libraries are often used in real-time market-data or event-driven architectures, where thousands or even millions of ticks/events may need to be processed very quickly.

In that context, using BigDecimal would have a significant performance and memory impact. BigDecimal is immutable, so every arithmetic operation creates a new object, which increases allocation pressure and garbage collection overhead. It also has a much larger memory footprint compared to double, roughly 40 bytes per value versus 8 bytes for a primitive double.

To mitigate this, the library avoids strict equality comparisons where precision matters and instead uses tolerance-based comparisons… with tolerances such as 1.0e-10, which are acceptable for the current use cases targeted by the library.”

Cuarenta bytes versus ocho. Millones de ticks. GC pressure. Comparaciones por tolerancia.

No era un descuido. Era una decisión de diseño fundamentada, apropiada para el dominio al que sirve la librería.

Esa respuesta me obligó a replantearme el dogma.


Por qué existe la regla

Antes de cuestionarla, vale la pena entender por qué existe.

double sigue el estándar IEEE 754 de punto flotante binario. El problema es que muchos decimales que parecen simples — como 0.1 — no tienen representación exacta en binario. El resultado es acumulación de error:

double a = 0.1 + 0.2;
System.out.println(a);          // 0.30000000000000004
System.out.println(a == 0.3);   // false

Para un sistema de facturación, esto es inaceptable. Si estás calculando el total de una orden, aplicando impuestos, o sumando transacciones en un estado de cuenta bancario, un centavo de diferencia acumulado en millones de operaciones puede ser un problema regulatorio, contable y de confianza del usuario.

La regla nació de ese contexto: transacciones monetarias donde el centavo importa y la exactitud decimal es un requisito del dominio.


Cuando la regla no aplica

El error es generalizar el contexto. Finanzas no es un dominio monolítico.

Hay por lo menos dos mundos completamente distintos dentro del universo financiero:

Mundo 1: Sistemas de dinero Nómina, facturación, contabilidad, pagos, impuestos, estados de cuenta. Aquí el centavo importa. La regulación exige exactitud. El usuario espera que $100.00 + $0.01 = $100.01, siempre.

Mundo 2: Cálculo cuantitativo Pricing de derivados, calibración de modelos, bootstrapping de curvas, simulaciones Monte Carlo, análisis de riesgo. Aquí se están resolviendo ecuaciones diferenciales, encontrando raíces de funciones no lineales, interpolando sobre cientos de puntos de la curva de tasas.

El segundo mundo se parece más a la física computacional que a la caja registradora.

En quant4j, cuando calculas la duración modificada de un bono o el YTM usando el método de Bisección, no estás sumando centavos. Estás aplicando algoritmos numéricos iterativos donde:

  1. La exactitud requerida es del orden de 1e-10, no de centavos
  2. El cálculo se ejecuta millones de veces por segundo en flujos de market data
  3. El dominio completo — libros de texto de finanzas matemáticas, simuladores, hojas de cálculo científicas — usa double (o float64) como tipo estándar

El costo real de BigDecimal en alto throughput

Cuando BigDecimal es immutable, cada operación aritmética crea un nuevo objeto en el heap.

// Cada operación de estas crea un nuevo objeto BigDecimal en el heap:
BigDecimal price = face
    .multiply(BigDecimal.valueOf(Math.exp(-rate * time.doubleValue())))
    .setScale(10, RoundingMode.HALF_UP);

Para una operación aislada, el costo es despreciable. Para un procesador de market data que recibe 500,000 ticks por segundo, cada uno requiriendo 10-20 operaciones aritméticas intermedias, el costo se vuelve medible y significativo:

  • Memoria: 40 bytes por BigDecimal vs 8 bytes por double primitivo. Una diferencia de 5x en el peor caso.
  • Allocation rate: mayor presión sobre el GC Young Generation.
  • Latencia de GC: en sistemas con latencia sensible (trading de alta frecuencia, procesamiento de riesgo en tiempo real), las pausas de GC son un problema real.
  • CPU: las operaciones de BigDecimal son significativamente más lentas que las de double, que se ejecutan directamente en instrucciones de punto flotante del procesador.

Esto no es una optimización prematura. Es una restricción del dominio.


La técnica de compensación: comparaciones por tolerancia (epsilon)

La pregunta legítima es: ¿cómo maneja la librería la imprecisión inherente del double?

La respuesta de Achraf lo explica: comparaciones por tolerancia, también conocidas como comparaciones epsilon.

En lugar de comparar por igualdad exacta — que con double es casi siempre incorrecto — se compara si la diferencia está dentro de un umbral aceptable para el dominio:

// MAL: comparación exacta con double
if (yield == targetYield) { ... }

// BIEN: comparación por tolerancia
static final double DEFAULT_TOLERANCE = 1.0e-10;

if (Math.abs(yield - targetYield) < DEFAULT_TOLERANCE) { ... }

quant4j usa este patrón consistentemente: TIME_EPSILON en el bootstrapping de curvas, DEFAULT_TOLERANCE en el solver de Bisección. Una tolerancia de 1e-10 es más que suficiente para los cálculos de finanzas cuantitativas, y órdenes de magnitud más precisa de lo que cualquier instrumento financiero real requiere.

Esta técnica es estándar en computación científica y numérica. Cualquier librería de álgebra lineal, simulación o análisis numérico la usa.


Cuándo sí usar BigDecimal

La regla sigue siendo válida en su contexto original. Usa BigDecimal cuando:

El dominio es transaccional y el centavo importa

// Correcto: precio de producto, impuesto, total de orden
BigDecimal subtotal = unitPrice.multiply(quantity);
BigDecimal tax = subtotal.multiply(TAX_RATE).setScale(2, RoundingMode.HALF_UP);
BigDecimal total = subtotal.add(tax);

Necesitas representación decimal exacta

0.1 en BigDecimal es exactamente 0.1. En double es 0.1000000000000000055511151231257827021181583404541015625.

El volumen de operaciones no es un restricción

Si calculas el precio de una factura o el salario mensual de un empleado, el overhead de BigDecimal es irrelevante.

La regulación o auditoría lo exige

Sistemas bancarios, nómina, facturación electrónica, sistemas contables. Aquí la exactitud decimal no es una preferencia técnica — es un requisito legal y de negocio.


Cuándo double puede ser la decisión correcta

Considera double cuando:

El dominio es numérico/científico, no transaccional

Pricing de opciones, simulaciones Monte Carlo, calibración de volatilidad implícita, modelos de riesgo, machine learning, procesamiento de señales. La ciencia y la ingeniería llevan décadas usando double con tolerancias epsilon.

El throughput es una restricción del sistema

Procesamiento de market data en tiempo real, sistemas de trading algorítmico, análisis de riesgo intraday. Si tu sistema necesita procesar millones de cálculos por segundo, double + comparaciones epsilon es la herramienta correcta.

Usas algoritmos numéricos iterativos

Métodos de Bisección, Newton-Raphson, Runge-Kutta, descomposición LU. Estos algoritmos están diseñados para double y la precisión de BigDecimal no agrega valor observable — el error de truncamiento del método supera con creces la imprecisión del punto flotante.

El error tolerable es mayor que la imprecisión del double

Si tu modelo acepta un error de 1e-6 y la imprecisión acumulada de double es 1e-15, BigDecimal no te está comprando nada útil.


La lección que me dejó el issue

Abrir ese issue con la certeza implícita de que double era un error fue, en retrospectiva, un ejemplo de aplicar una regla correcta fuera de su contexto.

Las mejores reglas de ingeniería — usa BigDecimal para dinero, evita la mutabilidad, prefiere interfaces sobre clases concretas — no son leyes universales. Son heurísticas que encapsulan una lección de un contexto específico.

La pregunta correcta no es “¿por qué no usas BigDecimal?” sino “¿cuáles son las restricciones de tu dominio y cómo afectan esta decisión?”.

Achraf eligió double porque su dominio requiere alto throughput, sus cálculos son numéricos (no transaccionales), y la precisión de 1e-10 con comparaciones epsilon es más que suficiente para sus casos de uso. Eso no es descuido — es diseño informado.

La próxima vez que veas double en código financiero, pregunta antes de asumir. El contexto importa más que la regla.


Resumen

ContextoTipo recomendadoRazón
Facturación, pagos, contabilidadBigDecimalExactitud decimal, regulación, centavos
Pricing cuantitativo, curvas de tasasdoubleThroughput, GC, cálculo numérico
Machine learning, simulacionesdouble / floatPerformance, compatibilidad con librerías
Tasas de impuesto, descuentosBigDecimalRedondeo determinístico requerido
Algoritmos numéricos iterativosdoubleDiseñados para IEEE 754
Sumas de transacciones en auditoríaBigDecimalTrazabilidad exacta

La regla no está mal. El error está en creer que todas las finanzas son el mismo dominio.