El problema fundamental que aborda este artículo es cómo construir y mantener sistemas distribuidos complejos y de misión crítica (especialmente en finanzas) en un entorno de hyper-crecimiento organizacional y técnico. La tesis central es que Haskell, a pesar de la sabiduría convencional que lo considera un lenguaje 'académico' o 'difícil' para la producción, ofrece herramientas poderosas para codificar el conocimiento operacional y las invariantes del sistema directamente en el código, lo que resulta en una mayor fiabilidad y capacidad de adaptación. Esto es especialmente relevante en empresas donde la rotación de personal es alta y el conocimiento institucional tiende a evaporarse.
La fiabilidad no se define solo como la ausencia de fallos, sino como la 'capacidad adaptativa' de un sistema para funcionar a pesar de la variación y los cambios. En un contexto de rápido crecimiento, donde la mitad del equipo siempre tiene menos de un año de experiencia, la capacidad de un sistema para ser comprensible, resistente a errores y fácil de operar se vuelve crítica. Haskell, con su sistema de tipos robusto, se posiciona como una herramienta para transformar la 'sabiduría tribal' en 'invariantes aplicadas por el compilador', haciendo que el camino seguro sea el más fácil y preservando el conocimiento institucional más allá de los individuos.
Arquitectura del Sistema
La arquitectura de Mercury, aunque no se detalla explícitamente, se infiere como un sistema distribuido con múltiples servicios que interactúan con bases de datos y sistemas externos. El artículo destaca el uso de Temporal como un framework de ejecución duradera para coordinar flujos de trabajo complejos que abarcan múltiples pasos, servicios y modos de fallo. Temporal permite escribir lógica de negocio secuencial ordinaria mientras la plataforma maneja la persistencia del estado, reintentos, timeouts y cancelación, reconstruyendo el estado del flujo de trabajo mediante la reproducción de un historial de eventos determinista. Esto reemplaza máquinas de estado manuales respaldadas por bases de datos y cron jobs, reduciendo la fragilidad operacional.
Se enfatiza la importancia de diseñar los modelos de dominio independientemente de la capa de transporte, utilizando tipos de error específicos del dominio en lugar de códigos de estado HTTP. Esto se logra mediante capas de traducción delgadas en los límites del sistema. Para la observabilidad y la extensibilidad, se promueve el patrón de 'registros de funciones' (records of functions) en lugar de funciones concretas, lo que permite la inyección de comportamiento (instrumentación, mocking, retries) en tiempo de ejecución sin modificar el código fuente de las librerías. Este patrón se alinea con el concepto de 'middleware' y 'interceptores', que a menudo forman un Monoid bajo composición, simplificando la combinación de preocupaciones transversales. Finalmente, se menciona el uso de unsafePerformIO en librerías de bajo nivel para optimizaciones de rendimiento, encapsulando la mutabilidad y manteniendo la contención de efectos secundarios.
Flujo de Transacción Durable con Temporal
- 1 Inicio Workflow Un evento externo o interno inicia un Workflow de Temporal.
- 2 Orquestación Workflow El código del Workflow (Haskell) define la secuencia lógica de pasos.
- 3 Actividad Externa El Workflow invoca una 'Actividad' (IO) para interactuar con sistemas externo...
- 4 Registro de Eventos Temporal registra el resultado de la Actividad en su historial de eventos.
- 5 Fallo/Crash Si el worker de Haskell falla o se reinicia.
- 6 Replay Determinista Otro worker de Temporal reproduce el historial de eventos para reconstruir el...
- 7 Continuación El Workflow continúa la ejecución desde el último punto registrado.
- 8 Finalización Workflow El Workflow completa todos sus pasos y finaliza.
| Capa | Tecnología | Justificación |
|---|---|---|
| compute | Haskell | Lenguaje de programación principal para la lógica de negocio y servicios backend, elegido por su sistema de tipos robusto que ayuda a codificar invariantes y mejorar la fiabilidad. |
| orchestration | Temporal | Framework de ejecución duradera para orquestar flujos de trabajo distribuidos y de larga duración, manejando reintentos, timeouts y persistencia de estado a través de un historial de eventos. vs Máquinas de estado manuales con base de datos y cron jobs hs-temporal-sdk (SDK de Haskell para Temporal, open-sourced por Mercury) |
| observability | OpenTelemetry | Estándar para instrumentación de telemetría (trazas, métricas, logs), utilizado para obtener visibilidad profunda en el comportamiento de los servicios en producción. hs-opentelemetry-api (API de Haskell para OpenTelemetry) |
Trade-offs
Ganancias
- ▲ Fiabilidad del sistema
- ▲ Mantenibilidad del código
- ▲ Preservación del conocimiento institucional
- ▲ Reducción de incidentes operacionales
- ▲ Facilidad de refactoring
Costes
- △ Velocidad de desarrollo inicial
- △ Curva de aprendizaje para nuevos ingenieros (Haskell)
- △ Rigidez del sistema de tipos (si se abusa)
- △ Costo cognitivo de tipos avanzados
- △ Madurez del ecosistema de librerías (en comparación con otros lenguajes)
data Transact a -- opaque; cannot be run directly
runTransactWithEvents :: (forall r. Transact r -> IO r) -> IO adata HttpClient = HttpClient {
sendRequest :: Request -> IO Response,
getManager :: IO Manager
}appTemporalInterceptors = mconcat [
retargetingInterceptor,
otelInterceptor,
sentryInterceptor,
sqlApplicationNameInterceptor,
loggingContextInterceptor,
statementTimeoutInterceptor,
teamNameInterceptor,
clientExceptionInterceptor,
workflowTypeNameInterceptor
]data PaymentError = InsufficientFunds | DuplicateRequest RequestId | PartnerTimeout Partner
toHttpError :: PaymentError -> HttpError
toHttpError InsufficientFunds = err402 "Insufficient funds"
toHttpError (DuplicateRequest _) = err409 "Duplicate request"
toHttpError (PartnerTimeout _) = err502 "Partner unavailable"
toWorkerStrategy :: PaymentError -> WorkerStrategy
toWorkerStrategy InsufficientFunds = Fail "Insufficient funds"
toWorkerStrategy (DuplicateRequest _) = Skip
toWorkerStrategy (PartnerTimeout _) = RetryWithBackoffFundamentos Teóricos
La filosofía de fiabilidad de Mercury se conecta directamente con los principios de la Ingeniería de Resiliencia, particularmente el trabajo de Erik Hollnagel sobre 'Safety-I and Safety-II' (Ashgate, 2014) y los conceptos de 'capacidad adaptativa' de David Woods (Resilience Engineering: Concepts and Precepts, Ashgate, 2006). Mientras que Safety-I se enfoca en prevenir fallos, Safety-II busca entender por qué los sistemas funcionan normalmente y mantener las condiciones para el éxito. Esta perspectiva influye en cómo Mercury aborda la fiabilidad, no solo eliminando errores, sino construyendo sistemas que pueden absorber variaciones.
El uso del sistema de tipos de Haskell para codificar invariantes y hacer que el 'camino seguro sea el fácil' resuena con la idea de 'diseño por contrato' (Design by Contract) popularizado por Bertrand Meyer, aunque aplicado a un nivel más profundo y estático a través de tipos avanzados. La discusión sobre la 'pureza como un límite' y la contención de efectos secundarios se alinea con los principios de encapsulación y separación de preocupaciones, fundamentales en la ingeniería de software. La adopción de Temporal para la ejecución duradera se basa en los principios de determinismo, replay y persistencia de eventos, conceptos que tienen raíces en sistemas distribuidos tolerantes a fallos y en el registro de transacciones (WAL) de bases de datos, permitiendo la reconstrucción del estado y la resiliencia ante fallos de procesos.