{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems","title":"TLA+ para Verificación Formal: Encontrando Bugs de Sistemas Distribuidos Antes de Escribir una Sola Línea de Código","description":"Modelando sistemas distribuidos con lógica temporal, corriendo el model checker TLC, y un ejemplo real de cómo TLA+ encontró un bug en un protocolo de rebalanceo de consumer groups de Kafka que el testing nunca habría encontrado.","language":"es","datePublished":"2026-03-18","dateModified":null,"contentFormat":"text/markdown","content":"Distributed Systems\n\n# TLA+ para Verificación Formal: Encontrando Bugs de Sistemas Distribuidos Antes de Escribir una Sola Línea de Código\n\n2026-03-187 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/formal-verification-tla-plus-distributed-systems>)\n\n## Fundamentos de TLA+\n\n[TLA+](<https://lamport.azurewebsites.net/tla/tla.html>) (Temporal Logic of Actions) es el lenguaje de especificación formal de Leslie Lamport. No escribirs código en TLA+, escribirs un modelo matemático de tu sistema como una máquina de estados. Después se ejecuta el model checker TLC, que explora exhaustivamente cada estado posible que tu sistema puede alcanzar y verifica que tus invariantes se cumplan en todos ellos.\n\nEl insight clave: los bugs de sistemas distribuidos no están en el código. Están en el *diseño*. Si tu protocolo está mal, ninguna cantidad de código cuidadoso o testing salvará. TLA+ encuentra bugs de diseño antes de que escribas una sola línea de código de implementación.\n\nUna especificación TLA+ mínima:\n\n```tla\n---- MODULE SimpleCounter ----\nEXTENDS Integers\n\nVARIABLE count\n\nInit == count = 0\n\nIncrement == count < 10 /\\ count' = count + 1\n\nNext == Increment\n\nSpec == Init /\\ [][Next]_count\n\nCountInvariant == count >= 0 /\\ count <= 10\n\n====\n```\n\n## PlusCal: TLA+ para Humanos\n\nTLA+ crudo es notación matemática. PlusCal es un lenguaje de algoritmos que transpila a TLA+ y se lee más como pseudocódigo:\n\n```tla\n---- MODULE MutualExclusion ----\nEXTENDS Integers, TLC\n\nCONSTANT NumProcesses\n\n(*--algorithm mutex\nvariables\n lock = \"free\";\n\nprocess proc \\in 1..NumProcesses\nvariables hasLock = FALSE;\nbegin\n AcquireLock:\n await lock = \"free\";\n lock := self;\n hasLock := TRUE;\n\n CriticalSection:\n skip;\n\n ReleaseLock:\n lock := \"free\";\n hasLock := FALSE;\n\n goto AcquireLock;\nend process;\nend algorithm; *)\n\nMutualExclusion ==\n \\A p1, p2 \\in 1..NumProcesses :\n (p1 /= p2) =>\n ~(pc[p1] = \"CriticalSection\" /\\ pc[p2] = \"CriticalSection\")\n\n====\n```\n\nEjecutar TLC con ` NumProcesses = 3 ` y va a explorar cada posible interleaving de los tres procesos. Si dos procese es pueden estar alguna vez en la sección crítica simultáneamente, TLC produce un contraejemplo, una secuencia exacta de estados que lleva a la violación.\n\n## El Ejemplo Real de Kafka\n\nLa siguiente especificación encontró un bug real. Modela una versión simplificada del protocolo de rebalanceo de consumer groups de Kafka:\n\n```tla\n---- MODULE KafkaRebalance ----\nEXTENDS Integers, Sequences, FiniteSets, TLC\n\nCONSTANTS\n Consumers, \\* Conjunto de IDs de consumers\n Partitions, \\* Conjunto de IDs de particiones\n MaxGenerations \\* Límite de números de generación\n\nVARIABLES\n groupState, \\* \"empty\" | \"rebalancing\" | \"stable\"\n generation, \\* Número de generación actual\n members, \\* Conjunto de consumers en el grupo\n leader, \\* Consumer leader actual (o \"none\")\n assignment, \\* Función: partición -> consumer (o \"unassigned\")\n consumerState, \\* Función: consumer -> estado\n pendingJoins, \\* Conjunto de consumers esperando unirse\n pendingSync \\* Conjunto esperando respuesta de sync\n\nvars == <<groupState, generation, members, leader, assignment,\n consumerState, pendingJoins, pendingSync>>\n\nTypeInvariant ==\n /\\ groupState \\in {\"empty\", \"rebalancing\", \"stable\"}\n /\\ generation \\in 0..MaxGenerations\n /\\ members \\subseteq Consumers\n /\\ leader \\in Consumers \\union {\"none\"}\n /\\ \\A p \\in Partitions : assignment[p] \\in Consumers \\union {\"unassigned\"}\n /\\ \\A c \\in Consumers : consumerState[c] \\in\n {\"idle\", \"joining\", \"active\", \"failed\"}\n\nInit ==\n /\\ groupState = \"empty\"\n /\\ generation = 0\n /\\ members = {}\n /\\ leader = \"none\"\n /\\ assignment = [p \\in Partitions |-> \"unassigned\"]\n /\\ consumerState = [c \\in Consumers |-> \"idle\"]\n /\\ pendingJoins = {}\n /\\ pendingSync = {}\n\n\\* Un consumer falla (crashea)\nConsumerFail(c) ==\n /\\ consumerState[c] \\in {\"joining\", \"active\"}\n /\\ consumerState' = [consumerState EXCEPT ![c] = \"failed\"]\n /\\ members' = members \\ {c}\n \\* EL BUG ESTABA ACÁ: assignment no se limpiaba inmediatamente\n \\* Original (buggy): UNCHANGED <<assignment>>\n \\* Fix: limpiar assignments del consumer fallido\n /\\ assignment' = [p \\in Partitions |->\n IF assignment[p] = c THEN \"unassigned\" ELSE assignment[p]]\n /\\ IF leader = c\n THEN leader' = IF members' /= {} THEN CHOOSE m \\in members' : TRUE\n ELSE \"none\"\n ELSE UNCHANGED leader\n /\\ UNCHANGED <<groupState, generation, pendingJoins, pendingSync>>\n\n\\* SAFETY: Ninguna partición asignada a un consumer fallido\nNoFailedAssignment ==\n \\A p \\in Partitions :\n assignment[p] /= \"unassigned\" =>\n consumerState[assignment[p]] /= \"failed\"\n\n\\* SAFETY: Todos los consumers asignados son miembros del grupo\nAssignedAreMembers ==\n \\A p \\in Partitions :\n assignment[p] /= \"unassigned\" => assignment[p] \\in members\n\n====\n```\n\n## El Bug que TLC Encontró\n\nAl ejecutar esto con el ` UNCHANGED <<assignment>> ` original en la acción ` ConsumerFail `, TLC encontró una violación de ` NoFailedAssignment ` en 6 estados:\n\n```\nError: Invariant NoFailedAssignment is violated.\n\nError trace:\n1: <Init>\n groupState = \"empty\", generation = 0, members = {}\n\n2: JoinGroup(\"c1\")\n consumerState[\"c1\"] = \"joining\"\n\n3: JoinGroup(\"c2\")\n consumerState[\"c2\"] = \"joining\"\n\n4: TriggerRebalance\n generation = 1, members = {\"c1\", \"c2\"}, leader = \"c1\"\n\n5: LeaderAssign\n assignment = [1 |-> \"c1\", 2 |-> \"c2\", 3 |-> \"c1\", 4 |-> \"c2\"]\n\n6: ConsumerFail(\"c2\")\n consumerState[\"c2\"] = \"failed\", members = {\"c1\"}\n assignment = [1 |-> \"c1\", 2 |-> \"c2\", 3 |-> \"c1\", 4 |-> \"c2\"]\n ^^^^^^^^^^^^^^^^^^^^^^^^ c2 está failed pero sigue asignado a particiones 2 y 4!\n```\n\nEl fix es obvio una vez que lo ves: cuando un consumer falla, limpiar inmediatamente sus asignaciones de particiones. Pero en la implementación real, la limpieza de assignments pasaba durante el *próximo* rebalanceo, no al momento de detección de falla. Esto significaba que había una ventana donde un nuevo consumer podía unirse, desencadenar un rebalanceo, y el algoritmo de asignación del leader podía ver la asignación vieja y tomar decisiones conflictivas.\n\nEste tipo de ventana puede existir en sistemas de producción durante meses. TLC la encuentra en 0.3 segundos.\n\n## Corriendo el Model Checker\n\n```bash\n# Crear archivo de config (KafkaRebalance.cfg)\ncat > KafkaRebalance.cfg << 'EOF'\nCONSTANTS\n Consumers = {\"c1\", \"c2\", \"c3\"}\n Partitions = {1, 2, 3, 4}\n MaxGenerations = 3\n\nINIT Init\nNEXT Next\n\nINVARIANTS\n TypeInvariant\n NoFailedAssignment\n AssignedAreMembers\nEOF\n\n# Correr TLC\njava -jar tla2tools.jar -config KafkaRebalance.cfg KafkaRebalance.tla\n\n# Output:\n# Model checking completed. No errors found.\n# 847,293 states generated, 234,521 distinct states found.\n# Finished in 00:00:04 (4 seconds)\n```\n\n847,293 estados. Cada posible interleaving de joins, fallas, rebalanceos y asignaciones de leader con 3 consumers, 4 particiones y hasta 3 generaciones. Explorado exhaustivamente en 4 segundos. Sin randomness, sin \"ojalá peguemos el bug\", certeza matemática de que los invariantes se cumplen.\n\n## Propiedades Temporales: Liveness\n\nLas propiedades de safety (\"cosas malas nunca pasan\") enganchan muchos bugs, pero las propiedades de liveness (\"cosas buenas eventualmente pasan\") enganchan una clase diferente:\n\n```tla\n\\* LIVENESS: Cada partición es eventualmente asignada a un consumer activo\nEventuallyAssigned ==\n \\A p \\in Partitions :\n <>(assignment[p] /= \"unassigned\" /\\\n consumerState[assignment[p]] = \"active\")\n\n\\* LIVENESS: El grupo eventualmente se estabiliza después de un rebalanceo\nEventuallyStable ==\n groupState = \"rebalancing\" ~> groupState = \"stable\"\n```\n\nEl operador ` <> ` significa \"eventualmente\". El operador ` ~> ` significa \"lleva a\". Estos requieren que TLC chequee propiedades temporales, lo que es más lento pero engancha bugs de liveness como bloqueo permanente o starvation.\n\n## Patrones Prácticos Reutilizables\n\n### Modelando Particiones de Red\n\n```tla\nVARIABLE connected\n\nNetworkPartition(n1, n2) ==\n /\\ connected' = [connected EXCEPT ![{n1, n2}] = FALSE]\n /\\ UNCHANGED <<otherVars>>\n\nSendMessage(from, to, msg) ==\n /\\ connected[{from, to}]\n /\\ \\* ... resto de la lógica de envío\n```\n\n### Modelando Pérdida y Reordenamiento de Mensajes\n\n```tla\nVARIABLE messages \\* Conjunto de mensajes en vuelo (desordenado por diseño)\n\nSend(msg) ==\n /\\ messages' = messages \\union {msg}\n\nReceive(msg) ==\n /\\ msg \\in messages\n /\\ messages' = messages \\ {msg}\n\n\\* Los mensajes también pueden perderse\nLoseMessage(msg) ==\n /\\ msg \\in messages\n /\\ messages' = messages \\ {msg}\n /\\ UNCHANGED <<otherVars>>\n```\n\n## Cuándo No Usar TLA+\n\nTLA+ es overkill para la mayoría del software. Utilizarlo cuando:\n\n1. **La correctitud es crítica y el testing es insuficiente.** Protocolos de consenso distribuido, estructuras de datos lock-free, niveles de aislamiento de transacciones.\n2. **El espacio de interacción es muy grande para testing.** 5 nodos, cada uno con 4 estados, 3 tipos de mensajes, falla en cualquier punto, son billones de interleavings.\n3. **El costo de un bug es alto.** Corrupción de datos, pérdida financiera, sistemas safety-critical.\n\nNo lo uses para:\n\n- Aplicaciones CRUD\n- Servicios request-response simples\n- Cualquier cosa donde la parte difícil es la UI o lógica de negocio y no el protocolo de concurrencia\n\nLa curva de aprendizaje es real, unas 3 semanas de estudio dedicado para ir de cero a productivo. Pero dado el historial de TLC encontrando bugs en protocolos que parecían obviamente correctos, modelar cada protocolo distribuido no trivial antes de implementar es una inversión sólida. Una spec usualmente lleva 2-3 días. Encontrar y arreglar un bug de concurrencia en producción lleva 2-3 semanas. La matemática es accesible.\n\n## Preguntas frecuentes\n\n### ¿Qué es TLA+ y por qué usarlo para sistemas distribuidos?\n\nTLA+ es un lenguaje de especificación formal basado en lógica temporal de acciones. Modelás tu sistema como una máquina de estados, se define invariantes, y se ejecuta el model checker TLC para explorar exhaustivamente todos los estados posibles. Encuentra bugs que el testing no puede, como condiciones de carrera que solo se manifiestan con un ordenamiento específico de mensajes entre 5 nodos.\n\n### ¿Es TLA+ práctico para equipos de ingeniería reales?\n\nSí. Amazon usa TLA+ extensivamente, encontraron bugs críticos en DynamoDB, S3 y EBS usando verificación formal. La curva de aprendizaje es empinada (2-3 semanas para ser productivo), pero para sistemas donde la correctitud es crítica, la inversión rinde dramáticamente.\n\n### ¿En qué se diferencia TLA+ del property-based testing?\n\nProperty-based testing genera inputs random y chequea propiedades contra tu código real. TLA+ chequea propiedades contra un modelo matemático de tu sistema explorando exhaustivamente todos los estados posibles. Property-based testing puede perder bugs escondidos en estados no explorados; TLA+ los encuentra todos.\n\n## Tips\n\n1. **Tip:** Comenzar con ` PlusCal ` en vez de TLA+ crudo. PlusCal es un lenguaje de algoritmos que transpila a TLA+ y se lee como pseudocódigo. Una vez que TLC encuentra un bug, se va a necesitar leer la traducción TLA+ para entender el error trace, pero para escribir specs, PlusCal te corta la curva de aprendizaje a la mitad.\n2. **Tip:** Mantener tu espacio de estados pequeño usando ` CONSTANTS ` con valores mínimos, 2 nodos, 3 mensajes máximo, 2 epochs. TLC explora cada estado alcanzable; pasar de 2 nodos a 3 puede aumentar el espacio de estados 100x. Solo subí los valores después de que tu spec pase con constantes pequeñas.\n3. **Tip:** Siempre agregar un ` TypeInvariant ` que restrinja cada variable a su dominio esperado. Sin él, un typo en tu spec (como asignar un string donde esperás un integer) crea millones de estados basura y TLC corre por horas antes de hacer OOM. El type invariant lo engancha en segundos.\n4. **Tip:** Ejecutar TLC con el flag ` -deadlock ` deshabilitado cuando recién empezás a modelar. Muchos diseños válidos de sistemas distribuidos tienen estados terminales (como todos los nodos apagándose) que TLC marca como deadlocks por defecto.\n\n- tla-plus\n- verificación-formal\n- sistemas-distribuidos\n- model-checking\n- lógica-temporal\n- kafka\n- consenso\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"TLA+ para Verificación Formal: Encontrando Bugs de Sistemas Distribuidos Antes de Escribir una Sola Línea de Código","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#title"},{"level":2,"text":"Fundamentos de TLA+","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#fundamentos-de-tla"},{"level":2,"text":"PlusCal: TLA+ para Humanos","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#pluscal-tla-para-humanos"},{"level":2,"text":"El Ejemplo Real de Kafka","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#el-ejemplo-real-de-kafka"},{"level":2,"text":"El Bug que TLC Encontró","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#el-bug-que-tlc-encontr"},{"level":2,"text":"Corriendo el Model Checker","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#corriendo-el-model-checker"},{"level":2,"text":"Propiedades Temporales: Liveness","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#propiedades-temporales-liveness"},{"level":2,"text":"Patrones Prácticos Reutilizables","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#patrones-prcticos-reutilizables"},{"level":3,"text":"Modelando Particiones de Red","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#modelando-particiones-de-red"},{"level":3,"text":"Modelando Pérdida y Reordenamiento de Mensajes","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#modelando-prdida-y-reordenamiento-de-mensajes"},{"level":2,"text":"Cuándo No Usar TLA+","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#cundo-no-usar-tla"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#questions"},{"level":3,"text":"¿Qué es TLA+ y por qué usarlo para sistemas distribuidos?","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#question-1"},{"level":3,"text":"¿Es TLA+ práctico para equipos de ingeniería reales?","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#question-2"},{"level":3,"text":"¿En qué se diferencia TLA+ del property-based testing?","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#article-tips"}],"sections":[{"heading":"TLA+ para Verificación Formal: Encontrando Bugs de Sistemas Distribuidos Antes de Escribir una Sola Línea de Código","level":1,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems","content":"Distributed Systems"},{"heading":"TLA+ para Verificación Formal: Encontrando Bugs de Sistemas Distribuidos Antes de Escribir una Sola Línea de Código","level":1,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#title","content":"2026-03-187 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/formal-verification-tla-plus-distributed-systems>)"},{"heading":"Fundamentos de TLA+","level":2,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#fundamentos-de-tla","content":"[TLA+](<https://lamport.azurewebsites.net/tla/tla.html>) (Temporal Logic of Actions) es el lenguaje de especificación formal de Leslie Lamport. No escribirs código en TLA+, escribirs un modelo matemático de tu sistema como una máquina de estados. Después se ejecuta el model checker TLC, que explora exhaustivamente cada estado posible que tu sistema puede alcanzar y verifica que tus invariantes se cumplan en todos ellos.\n\nEl insight clave: los bugs de sistemas distribuidos no están en el código. Están en el *diseño*. Si tu protocolo está mal, ninguna cantidad de código cuidadoso o testing salvará. TLA+ encuentra bugs de diseño antes de que escribas una sola línea de código de implementación.\n\nUna especificación TLA+ mínima:\n\n```tla\n---- MODULE SimpleCounter ----\nEXTENDS Integers\n\nVARIABLE count\n\nInit == count = 0\n\nIncrement == count < 10 /\\ count' = count + 1\n\nNext == Increment\n\nSpec == Init /\\ [][Next]_count\n\nCountInvariant == count >= 0 /\\ count <= 10\n\n====\n```"},{"heading":"PlusCal: TLA+ para Humanos","level":2,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#pluscal-tla-para-humanos","content":"TLA+ crudo es notación matemática. PlusCal es un lenguaje de algoritmos que transpila a TLA+ y se lee más como pseudocódigo:\n\n```tla\n---- MODULE MutualExclusion ----\nEXTENDS Integers, TLC\n\nCONSTANT NumProcesses\n\n(*--algorithm mutex\nvariables\n lock = \"free\";\n\nprocess proc \\in 1..NumProcesses\nvariables hasLock = FALSE;\nbegin\n AcquireLock:\n await lock = \"free\";\n lock := self;\n hasLock := TRUE;\n\n CriticalSection:\n skip;\n\n ReleaseLock:\n lock := \"free\";\n hasLock := FALSE;\n\n goto AcquireLock;\nend process;\nend algorithm; *)\n\nMutualExclusion ==\n \\A p1, p2 \\in 1..NumProcesses :\n (p1 /= p2) =>\n ~(pc[p1] = \"CriticalSection\" /\\ pc[p2] = \"CriticalSection\")\n\n====\n```\n\nEjecutar TLC con ` NumProcesses = 3 ` y va a explorar cada posible interleaving de los tres procesos. Si dos procese es pueden estar alguna vez en la sección crítica simultáneamente, TLC produce un contraejemplo, una secuencia exacta de estados que lleva a la violación."},{"heading":"El Ejemplo Real de Kafka","level":2,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#el-ejemplo-real-de-kafka","content":"La siguiente especificación encontró un bug real. Modela una versión simplificada del protocolo de rebalanceo de consumer groups de Kafka:\n\n```tla\n---- MODULE KafkaRebalance ----\nEXTENDS Integers, Sequences, FiniteSets, TLC\n\nCONSTANTS\n Consumers, \\* Conjunto de IDs de consumers\n Partitions, \\* Conjunto de IDs de particiones\n MaxGenerations \\* Límite de números de generación\n\nVARIABLES\n groupState, \\* \"empty\" | \"rebalancing\" | \"stable\"\n generation, \\* Número de generación actual\n members, \\* Conjunto de consumers en el grupo\n leader, \\* Consumer leader actual (o \"none\")\n assignment, \\* Función: partición -> consumer (o \"unassigned\")\n consumerState, \\* Función: consumer -> estado\n pendingJoins, \\* Conjunto de consumers esperando unirse\n pendingSync \\* Conjunto esperando respuesta de sync\n\nvars == <<groupState, generation, members, leader, assignment,\n consumerState, pendingJoins, pendingSync>>\n\nTypeInvariant ==\n /\\ groupState \\in {\"empty\", \"rebalancing\", \"stable\"}\n /\\ generation \\in 0..MaxGenerations\n /\\ members \\subseteq Consumers\n /\\ leader \\in Consumers \\union {\"none\"}\n /\\ \\A p \\in Partitions : assignment[p] \\in Consumers \\union {\"unassigned\"}\n /\\ \\A c \\in Consumers : consumerState[c] \\in\n {\"idle\", \"joining\", \"active\", \"failed\"}\n\nInit ==\n /\\ groupState = \"empty\"\n /\\ generation = 0\n /\\ members = {}\n /\\ leader = \"none\"\n /\\ assignment = [p \\in Partitions |-> \"unassigned\"]\n /\\ consumerState = [c \\in Consumers |-> \"idle\"]\n /\\ pendingJoins = {}\n /\\ pendingSync = {}\n\n\\* Un consumer falla (crashea)\nConsumerFail(c) ==\n /\\ consumerState[c] \\in {\"joining\", \"active\"}\n /\\ consumerState' = [consumerState EXCEPT ![c] = \"failed\"]\n /\\ members' = members \\ {c}\n \\* EL BUG ESTABA ACÁ: assignment no se limpiaba inmediatamente\n \\* Original (buggy): UNCHANGED <<assignment>>\n \\* Fix: limpiar assignments del consumer fallido\n /\\ assignment' = [p \\in Partitions |->\n IF assignment[p] = c THEN \"unassigned\" ELSE assignment[p]]\n /\\ IF leader = c\n THEN leader' = IF members' /= {} THEN CHOOSE m \\in members' : TRUE\n ELSE \"none\"\n ELSE UNCHANGED leader\n /\\ UNCHANGED <<groupState, generation, pendingJoins, pendingSync>>\n\n\\* SAFETY: Ninguna partición asignada a un consumer fallido\nNoFailedAssignment ==\n \\A p \\in Partitions :\n assignment[p] /= \"unassigned\" =>\n consumerState[assignment[p]] /= \"failed\"\n\n\\* SAFETY: Todos los consumers asignados son miembros del grupo\nAssignedAreMembers ==\n \\A p \\in Partitions :\n assignment[p] /= \"unassigned\" => assignment[p] \\in members\n\n====\n```"},{"heading":"El Bug que TLC Encontró","level":2,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#el-bug-que-tlc-encontr","content":"Al ejecutar esto con el ` UNCHANGED <<assignment>> ` original en la acción ` ConsumerFail `, TLC encontró una violación de ` NoFailedAssignment ` en 6 estados:\n\n```\nError: Invariant NoFailedAssignment is violated.\n\nError trace:\n1: <Init>\n groupState = \"empty\", generation = 0, members = {}\n\n2: JoinGroup(\"c1\")\n consumerState[\"c1\"] = \"joining\"\n\n3: JoinGroup(\"c2\")\n consumerState[\"c2\"] = \"joining\"\n\n4: TriggerRebalance\n generation = 1, members = {\"c1\", \"c2\"}, leader = \"c1\"\n\n5: LeaderAssign\n assignment = [1 |-> \"c1\", 2 |-> \"c2\", 3 |-> \"c1\", 4 |-> \"c2\"]\n\n6: ConsumerFail(\"c2\")\n consumerState[\"c2\"] = \"failed\", members = {\"c1\"}\n assignment = [1 |-> \"c1\", 2 |-> \"c2\", 3 |-> \"c1\", 4 |-> \"c2\"]\n ^^^^^^^^^^^^^^^^^^^^^^^^ c2 está failed pero sigue asignado a particiones 2 y 4!\n```\n\nEl fix es obvio una vez que lo ves: cuando un consumer falla, limpiar inmediatamente sus asignaciones de particiones. Pero en la implementación real, la limpieza de assignments pasaba durante el *próximo* rebalanceo, no al momento de detección de falla. Esto significaba que había una ventana donde un nuevo consumer podía unirse, desencadenar un rebalanceo, y el algoritmo de asignación del leader podía ver la asignación vieja y tomar decisiones conflictivas.\n\nEste tipo de ventana puede existir en sistemas de producción durante meses. TLC la encuentra en 0.3 segundos."},{"heading":"Corriendo el Model Checker","level":2,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#corriendo-el-model-checker","content":"```bash\n# Crear archivo de config (KafkaRebalance.cfg)\ncat > KafkaRebalance.cfg << 'EOF'\nCONSTANTS\n Consumers = {\"c1\", \"c2\", \"c3\"}\n Partitions = {1, 2, 3, 4}\n MaxGenerations = 3\n\nINIT Init\nNEXT Next\n\nINVARIANTS\n TypeInvariant\n NoFailedAssignment\n AssignedAreMembers\nEOF\n\n# Correr TLC\njava -jar tla2tools.jar -config KafkaRebalance.cfg KafkaRebalance.tla\n\n# Output:\n# Model checking completed. No errors found.\n# 847,293 states generated, 234,521 distinct states found.\n# Finished in 00:00:04 (4 seconds)\n```\n\n847,293 estados. Cada posible interleaving de joins, fallas, rebalanceos y asignaciones de leader con 3 consumers, 4 particiones y hasta 3 generaciones. Explorado exhaustivamente en 4 segundos. Sin randomness, sin \"ojalá peguemos el bug\", certeza matemática de que los invariantes se cumplen."},{"heading":"Propiedades Temporales: Liveness","level":2,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#propiedades-temporales-liveness","content":"Las propiedades de safety (\"cosas malas nunca pasan\") enganchan muchos bugs, pero las propiedades de liveness (\"cosas buenas eventualmente pasan\") enganchan una clase diferente:\n\n```tla\n\\* LIVENESS: Cada partición es eventualmente asignada a un consumer activo\nEventuallyAssigned ==\n \\A p \\in Partitions :\n <>(assignment[p] /= \"unassigned\" /\\\n consumerState[assignment[p]] = \"active\")\n\n\\* LIVENESS: El grupo eventualmente se estabiliza después de un rebalanceo\nEventuallyStable ==\n groupState = \"rebalancing\" ~> groupState = \"stable\"\n```\n\nEl operador ` <> ` significa \"eventualmente\". El operador ` ~> ` significa \"lleva a\". Estos requieren que TLC chequee propiedades temporales, lo que es más lento pero engancha bugs de liveness como bloqueo permanente o starvation."},{"heading":"Patrones Prácticos Reutilizables","level":2,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#patrones-prcticos-reutilizables","content":""},{"heading":"Modelando Particiones de Red","level":3,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#modelando-particiones-de-red","content":"```tla\nVARIABLE connected\n\nNetworkPartition(n1, n2) ==\n /\\ connected' = [connected EXCEPT ![{n1, n2}] = FALSE]\n /\\ UNCHANGED <<otherVars>>\n\nSendMessage(from, to, msg) ==\n /\\ connected[{from, to}]\n /\\ \\* ... resto de la lógica de envío\n```"},{"heading":"Modelando Pérdida y Reordenamiento de Mensajes","level":3,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#modelando-prdida-y-reordenamiento-de-mensajes","content":"```tla\nVARIABLE messages \\* Conjunto de mensajes en vuelo (desordenado por diseño)\n\nSend(msg) ==\n /\\ messages' = messages \\union {msg}\n\nReceive(msg) ==\n /\\ msg \\in messages\n /\\ messages' = messages \\ {msg}\n\n\\* Los mensajes también pueden perderse\nLoseMessage(msg) ==\n /\\ msg \\in messages\n /\\ messages' = messages \\ {msg}\n /\\ UNCHANGED <<otherVars>>\n```"},{"heading":"Cuándo No Usar TLA+","level":2,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#cundo-no-usar-tla","content":"TLA+ es overkill para la mayoría del software. Utilizarlo cuando:\n\n1. **La correctitud es crítica y el testing es insuficiente.** Protocolos de consenso distribuido, estructuras de datos lock-free, niveles de aislamiento de transacciones.\n2. **El espacio de interacción es muy grande para testing.** 5 nodos, cada uno con 4 estados, 3 tipos de mensajes, falla en cualquier punto, son billones de interleavings.\n3. **El costo de un bug es alto.** Corrupción de datos, pérdida financiera, sistemas safety-critical.\n\nNo lo uses para:\n\n- Aplicaciones CRUD\n- Servicios request-response simples\n- Cualquier cosa donde la parte difícil es la UI o lógica de negocio y no el protocolo de concurrencia\n\nLa curva de aprendizaje es real, unas 3 semanas de estudio dedicado para ir de cero a productivo. Pero dado el historial de TLC encontrando bugs en protocolos que parecían obviamente correctos, modelar cada protocolo distribuido no trivial antes de implementar es una inversión sólida. Una spec usualmente lleva 2-3 días. Encontrar y arreglar un bug de concurrencia en producción lleva 2-3 semanas. La matemática es accesible."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#questions","content":""},{"heading":"¿Qué es TLA+ y por qué usarlo para sistemas distribuidos?","level":3,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#question-1","content":"TLA+ es un lenguaje de especificación formal basado en lógica temporal de acciones. Modelás tu sistema como una máquina de estados, se define invariantes, y se ejecuta el model checker TLC para explorar exhaustivamente todos los estados posibles. Encuentra bugs que el testing no puede, como condiciones de carrera que solo se manifiestan con un ordenamiento específico de mensajes entre 5 nodos."},{"heading":"¿Es TLA+ práctico para equipos de ingeniería reales?","level":3,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#question-2","content":"Sí. Amazon usa TLA+ extensivamente, encontraron bugs críticos en DynamoDB, S3 y EBS usando verificación formal. La curva de aprendizaje es empinada (2-3 semanas para ser productivo), pero para sistemas donde la correctitud es crítica, la inversión rinde dramáticamente."},{"heading":"¿En qué se diferencia TLA+ del property-based testing?","level":3,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#question-3","content":"Property-based testing genera inputs random y chequea propiedades contra tu código real. TLA+ chequea propiedades contra un modelo matemático de tu sistema explorando exhaustivamente todos los estados posibles. Property-based testing puede perder bugs escondidos en estados no explorados; TLA+ los encuentra todos."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#article-tips","content":"1. **Tip:** Comenzar con ` PlusCal ` en vez de TLA+ crudo. PlusCal es un lenguaje de algoritmos que transpila a TLA+ y se lee como pseudocódigo. Una vez que TLC encuentra un bug, se va a necesitar leer la traducción TLA+ para entender el error trace, pero para escribir specs, PlusCal te corta la curva de aprendizaje a la mitad.\n2. **Tip:** Mantener tu espacio de estados pequeño usando ` CONSTANTS ` con valores mínimos, 2 nodos, 3 mensajes máximo, 2 epochs. TLC explora cada estado alcanzable; pasar de 2 nodos a 3 puede aumentar el espacio de estados 100x. Solo subí los valores después de que tu spec pase con constantes pequeñas.\n3. **Tip:** Siempre agregar un ` TypeInvariant ` que restrinja cada variable a su dominio esperado. Sin él, un typo en tu spec (como asignar un string donde esperás un integer) crea millones de estados basura y TLC corre por horas antes de hacer OOM. El type invariant lo engancha en segundos.\n4. **Tip:** Ejecutar TLC con el flag ` -deadlock ` deshabilitado cuando recién empezás a modelar. Muchos diseños válidos de sistemas distribuidos tienen estados terminales (como todos los nodos apagándose) que TLC marca como deadlocks por defecto.\n\n- tla-plus\n- verificación-formal\n- sistemas-distribuidos\n- model-checking\n- lógica-temporal\n- kafka\n- consenso\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/formal-verification-tla-plus-distributed-systems"},{"text":"TLA+","url":"https://lamport.azurewebsites.net/tla/tla.html"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems"},"headline":"TLA+ para Verificación Formal: Encontrando Bugs de Sistemas Distribuidos Antes de Escribir una Sola Línea de Código","description":"Modelando sistemas distribuidos con lógica temporal, corriendo el model checker TLC, y un ejemplo real de cómo TLA+ encontró un bug en un protocolo de rebalanceo de consumer groups de Kafka que el testing nunca habría encontrado.","datePublished":"2026-03-18","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://statics.lucioduran.com/images/lucioduran-social-card.jpg","url":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Distributed Systems","keywords":"tla-plus, verificación-formal, sistemas-distribuidos, model-checking, lógica-temporal, kafka, consenso","wordCount":1340,"timeRequired":"PT7M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/formal-verification-tla-plus-distributed-systems"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"TLA+ para Verificación Formal: Encontrando Bugs de Sistemas Distribuidos Antes de Escribir una Sola Línea de Código","item":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Qué es TLA+ y por qué usarlo para sistemas distribuidos?","acceptedAnswer":{"@type":"Answer","text":"TLA+ es un lenguaje de especificación formal basado en lógica temporal de acciones. Modelás tu sistema como una máquina de estados, se define invariantes, y se ejecuta el model checker TLC para explorar exhaustivamente todos los estados posibles. Encuentra bugs que el testing no puede, como condiciones de carrera que solo se manifiestan con un ordenamiento específico de mensajes entre 5 nodos."}},{"@type":"Question","name":"¿Es TLA+ práctico para equipos de ingeniería reales?","acceptedAnswer":{"@type":"Answer","text":"Sí. Amazon usa TLA+ extensivamente, encontraron bugs críticos en DynamoDB, S3 y EBS usando verificación formal. La curva de aprendizaje es empinada (2-3 semanas para ser productivo), pero para sistemas donde la correctitud es crítica, la inversión rinde dramáticamente."}},{"@type":"Question","name":"¿En qué se diferencia TLA+ del property-based testing?","acceptedAnswer":{"@type":"Answer","text":"Property-based testing genera inputs random y chequea propiedades contra tu código real. TLA+ chequea propiedades contra un modelo matemático de tu sistema explorando exhaustivamente todos los estados posibles. Property-based testing puede perder bugs escondidos en estados no explorados; TLA+ los encuentra todos."}}]}]}],"contentHash":"9dfd86f225b78e161d717d77feb3179c5f5406db14cf8c4adebd27d75132ceea","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/formal-verification-tla-plus-distributed-systems","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":65764,"contentBytes":12362}}
