Las ideas de programación estructurada de Edsger Dijkstra explican por qué el código disciplinado y simple se mantiene correcto y mantenible a medida que crecen los equipos, funcionalidades y sistemas.

El software rara vez falla porque no pueda escribirse. Falla porque, un año después, nadie puede cambiarlo de forma segura.
A medida que las bases de código crecen, cada ajuste “pequeño” empieza a ramificarse: una corrección rompe una funcionalidad lejana, un nuevo requisito fuerza reescrituras, y un simple refactor se convierte en una semana de coordinación cuidadosa. La parte difícil no es añadir código: es mantener el comportamiento predecible mientras todo lo demás cambia.
Edsger Dijkstra defendía que la corrección y la simplicidad deberían ser objetivos de primera clase, no lujos. La ganancia no es académica. Cuando un sistema es más fácil de razonar, los equipos pasan menos tiempo apagando incendios y más tiempo construyendo.
Cuando la gente dice que el software debe “escalar”, a menudo se refieren al rendimiento. El punto de Dijkstra es distinto: la complejidad también escala.
La escala aparece como:
La programación estructurada no se trata de ser estricto por sí misma. Se trata de elegir el flujo de control y la descomposición que faciliten responder dos preguntas:
Cuando el comportamiento es predecible, cambiar deja de ser arriesgado y pasa a ser rutinario. Por eso Dijkstra sigue siendo relevante: su disciplina apunta al verdadero cuello de botella del software en crecimiento: entenderlo lo suficiente como para mejorarlo.
Edsger W. Dijkstra (1930–2002) fue un científico de la computación neerlandés que ayudó a moldear cómo los programadores piensan sobre construir software fiable. Trabajó en sistemas operativos tempranos, contribuyó a algoritmos (incluido el algoritmo de ruta más corta que lleva su nombre) y—lo más importante para los desarrolladores cotidianos—promovió la idea de que programar debería ser algo sobre lo que podamos razonar, no solo algo que intentemos hasta que parece funcionar.
A Dijkstra le importaba menos si un programa podía producir la salida correcta para algunos ejemplos y más si podíamos explicar por qué es correcto para los casos que importan.
Si puedes enunciar lo que una porción de código debe hacer, deberías poder argumentar (paso a paso) que realmente lo hace. Esa mentalidad conduce de forma natural a código más fácil de seguir, revisar y menos dependiente de depuraciones heroicas.
Algunos de los escritos de Dijkstra suenan implacables. Criticaba los trucos “ingeniosos”, los flujos de control descuidados y las costumbres de codificación que dificultan el razonamiento. La rigidez no busca imponer estilo; busca reducir la ambigüedad. Cuando el significado del código es claro, se pierde menos tiempo debatiendo intenciones y se gana tiempo validando comportamientos.
La programación estructurada es la práctica de construir programas a partir de un pequeño conjunto de estructuras de control claras—secuencia, selección (if/else) y iteración (bucles)—en vez de saltos enmarañados en el flujo. La meta es simple: hacer que el camino a través del programa sea comprensible para que puedas explicarlo, mantenerlo y cambiarlo con confianza.
La gente suele describir la calidad del software como “rápido”, “bonito” o “rico en funcionalidades”. Los usuarios experimentan la corrección de otra forma: como la confianza tranquila de que la app no les sorprenderá. Cuando la corrección está presente, nadie la nota. Cuando falta, todo lo demás deja de importar.
“Funciona ahora” suele significar que probaste algunos caminos y obtuviste el resultado esperado. “Sigue funcionando” significa que se comporta como se pretende a lo largo del tiempo, ante casos límite y cambios—después de refactors, nuevas integraciones, mayor tráfico y nuevos miembros del equipo que tocan el código.
Una funcionalidad puede “funcionar ahora” y aun así ser frágil:
La corrección trata de eliminar esos supuestos ocultos—o hacerlos explícitos.
Un bug menor rara vez se mantiene menor cuando el software crece. Un estado incorrecto, un límite off-by-one o una regla de manejo de errores poco clara se copia en nuevos módulos, se envuelve por otros servicios, se cachea, se reintenta o se “parchea”. Con el tiempo, los equipos dejan de preguntar “¿qué es verdadero?” y empiezan a preguntar “¿qué suele pasar?” Ahí es cuando la respuesta a incidentes se convierte en arqueología.
El multiplicador es la dependencia: un pequeño mal comportamiento se convierte en muchos comportamientos descendentes, cada uno con su parche parcial.
El código claro mejora la corrección porque mejora la comunicación:
La corrección significa: para las entradas y situaciones que decimos soportar, el sistema produce consistentemente los resultados que prometemos—y falla de formas previsibles y explicables cuando no puede.
La simplicidad no busca que el código sea “bonito”, minimalista o ingenioso. Busca que el comportamiento sea fácil de predecir, explicar y modificar sin miedo. Dijkstra valoró la simplicidad porque mejora nuestra capacidad para razonar sobre programas—especialmente cuando la base de código y el equipo crecen.
El código simple mantiene un número reducido de ideas en movimiento a la vez: flujo de datos claro, flujo de control claro y responsabilidades definidas. No obliga al lector a simular muchas rutas alternas en la cabeza.
La simplicidad no es:
Muchos sistemas se vuelven difíciles de cambiar no porque el dominio sea inherentemente complejo, sino porque introducimos complejidad accidental: flags que interactúan de formas inesperadas, parches de casos especiales que nunca se eliminan y capas que existen mayormente para sortear decisiones anteriores.
Cada excepción extra es un impuesto sobre la comprensión. El coste aparece más tarde, cuando alguien intenta arreglar un bug y descubre que un cambio en un área rompe sutilmente varias otras.
Cuando un diseño es simple, el progreso viene del trabajo constante: cambios revisables, diffs más pequeños y menos arreglos de emergencia. Los equipos no necesitan desarrolladores “héroe” que recuerden cada caso límite histórico o que deban depurar a las 2 a.m. bajo presión. En su lugar, el sistema soporta la atención humana normal.
Una prueba práctica: si sigues añadiendo excepciones (“a menos que…”, “excepto cuando…”, “solo para este cliente…”), probablemente estás acumulando complejidad accidental. Prefiere soluciones que reduzcan el branching en el comportamiento—una regla consistente suele vencer a cinco casos especiales, incluso si la regla consistente es algo más general de lo que imaginabas al principio.
La programación estructurada es una idea simple con grandes consecuencias: escribe código de modo que su ruta de ejecución sea fácil de seguir. En términos sencillos, la mayoría de programas se puede construir con tres bloques—secuencia, selección y repetición—sin depender de saltos enmarañados.
if/else, switch).for, while).Cuando el flujo de control se compone con estas estructuras, normalmente puedes explicar lo que hace el programa leyéndolo de arriba a abajo, sin “teletransportarte” por el archivo.
Antes de que la programación estructurada se convirtiera en norma, muchas bases de código dependían fuertemente de saltos arbitrarios (control al estilo goto). El problema no es que los saltos sean siempre malos; es que los saltos sin restricciones crean rutas de ejecución difíciles de predecir. Terminas preguntando “¿Cómo llegamos aquí?” y “¿En qué estado está esta variable?”—y el código no responde claramente.
El flujo de control claro ayuda a los humanos a construir un modelo mental correcto. Ese modelo es en lo que confías cuando depuras, revisas un pull request o cambias comportamiento bajo presión de tiempo.
Cuando la estructura es consistente, la modificación se vuelve más segura: puedes cambiar una rama sin afectar accidentalmente otra, o refactorizar un bucle sin perder una salida oculta. La legibilidad no es solo estética: es la base para cambiar comportamiento con confianza sin romper lo que ya funciona.
Dijkstra impulsó una idea simple: si puedes explicar por qué tu código es correcto, puedes cambiarlo con menos miedo. Tres pequeñas herramientas de razonamiento hacen eso práctico—sin convertir a tu equipo en matemáticos.
Un invariante es un hecho que permanece cierto mientras un fragmento de código se ejecuta, especialmente dentro de un bucle.
Ejemplo: sumas precios en un carrito. Un invariante útil es: “total equals the sum of all items processed so far.” Si eso se mantiene en cada paso, al finalizar el bucle el resultado es confiable.
Los invariantes son poderosos porque enfocan la atención en lo que nunca debe romperse, no solo en lo que debe pasar a continuación.
Una precondición es lo que debe ser cierto antes de que una función se ejecute. Una postcondición es lo que la función garantiza después de terminar.
Ejemplos cotidianos:
En código, una precondición podría ser “la lista de entrada está ordenada” y la postcondición “la lista de salida está ordenada y contiene los mismos elementos más el insertado”.
Cuando los escribes (aunque sea informalmente), el diseño se afina: decides qué espera y qué promete la función, y naturalmente la haces más pequeña y enfocada.
En las revisiones, desplaza el debate lejos del estilo (“lo escribiría distinto”) hacia la corrección (“¿mantiene este invariante?” “¿Imponemos la precondición o la documentamos?”).
No necesitas pruebas formales para beneficiarte. Elige el bucle más propenso a bugs o la actualización de estado más compleja y añade un comentario de una línea con un invariante sobre él. Cuando alguien edite después, ese comentario actúa como barandilla: si un cambio rompe ese hecho, el código ya no es seguro.
Pruebas y razonamiento buscan el mismo resultado—software que se comporte como se pretende—pero operan de formas muy distintas. Las pruebas descubren problemas intentando ejemplos. El razonamiento previene categorías enteras de problemas haciendo la lógica explícita y comprobable.
Las pruebas son una red de seguridad práctica. Detectan regresiones, verifican escenarios reales y documentan el comportamiento esperado de forma ejecutable por todo el equipo.
Pero las pruebas solo pueden mostrar la presencia de bugs, no su ausencia. Ninguna suite de pruebas cubre cada entrada, cada variación temporal o cada interacción entre características. Muchos fallos “funciona en mi máquina” vienen de combinaciones no probadas: una entrada rara, un orden específico de operaciones o un estado sutil que aparece después de varios pasos.
El razonamiento trata de probar propiedades del código: “este bucle siempre termina”, “esta variable nunca es negativa”, “esta función nunca devuelve un objeto inválido”. Bien hecho, descarta clases enteras de defectos—especialmente en fronteras y casos límite.
La limitación es el esfuerzo y el alcance. Pruebas formales completas para un producto entero rara vez son económicas. El razonamiento funciona mejor aplicado selectivamente: algoritmos centrales, flujos sensibles a seguridad, lógica de pago y concurrencia.
Usa pruebas ampliamente y aplica razonamiento profundo donde fallar es caro.
Un puente práctico entre ambos es hacer ejecutable la intención:
Estas técnicas no reemplazan pruebas: tensan la red. Convierten expectativas vagas en reglas comprobables, haciendo más difícil escribir bugs y más fácil diagnosticarlos.
El código “ingenioso” suele sentirse como una victoria momentánea: menos líneas, un truco elegante, un one-liner que te hace sentir inteligente. El problema es que la astucia no escala en el tiempo ni entre personas. Seis meses después, el autor olvida el truco. Un compañero nuevo lo lee literalmente, ignora un supuesto oculto y lo cambia de manera que rompe el comportamiento. Eso es “deuda por astucia”: velocidad a corto plazo comprada con confusión a largo plazo.
El punto de Dijkstra no era “escribe código aburrido” por preferencia de estilo: era que las restricciones disciplinadas hacen que los programas sean más fáciles de razonar. En un equipo, las restricciones también reducen la fatiga de decisión. Si todos ya conocen los valores por defecto (cómo nombrar, cómo estructurar funciones, qué significa “hecho”), dejas de reabrir lo básico en cada pull request. Ese tiempo vuelve al trabajo de producto.
La disciplina aparece en prácticas rutinarias:
Algunos hábitos concretos previenen que la deuda por astucia se acumule:
calculate_total() sobre do_it()).La disciplina no busca la perfección: busca hacer predecible el siguiente cambio.
La modularidad no es solo “dividir código en archivos”. Es aislar decisiones detrás de fronteras claras para que el resto del sistema no necesite saber (o preocuparse por) los detalles internos. Un módulo oculta las partes enmarañadas—estructuras de datos, casos límite, trucos de rendimiento—y expone una superficie pequeña y estable.
Cuando llega una petición de cambio, el resultado ideal es: un módulo cambia y todo lo demás queda intacto. Ese es el significado práctico de “mantener el cambio local”. Las fronteras previenen el acoplamiento accidental—donde actualizar una característica rompe tres otras porque compartían supuestos.
Una buena frontera también facilita el razonamiento. Si puedes enunciar lo que un módulo garantiza, puedes razonar sobre el programa mayor sin releer toda su implementación cada vez.
Una interfaz es una promesa: “Dado esto input, devolveré este output y mantendré estas reglas.” Cuando la promesa es clara, los equipos pueden trabajar en paralelo:
No se trata de burocracia: se trata de crear puntos de coordinación seguros en una base de código que crece.
No necesitas una gran revisión de arquitectura para mejorar la modularidad. Prueba estas comprobaciones ligeras:
Fronteras bien dibujadas convierten “cambio” de un evento del sistema en una edición localizada.
Cuando el software es pequeño, puedes “tenerlo todo en la cabeza”. A escala, eso deja de ser cierto—y los modos de fallo se vuelven familiares.
Síntomas comunes:
La apuesta principal de Dijkstra era que los humanos son el cuello de botella. Flujo de control claro, unidades pequeñas bien definidas y código que puedas razonar no son opciones estéticas: son multiplicadores de capacidad.
En una base de código grande, la estructura actúa como compresión para la comprensión. Si las funciones tienen inputs/outputs explícitos, los módulos tienen fronteras que puedes nombrar y la “ruta feliz” no está mezclada con cada caso límite, los desarrolladores gastan menos tiempo reconstruyendo intención y más tiempo en cambios deliberados.
A medida que crecen los equipos, los costes de comunicación suben más rápido que las líneas de código. El código disciplinado y legible reduce la cantidad de conocimiento tribal necesario para contribuir con seguridad.
Eso se nota de inmediato en la incorporación: los ingenieros nuevos siguen patrones predecibles, aprenden un pequeño conjunto de convenciones y hacen cambios sin necesitar un largo tour de “trampas”. El propio código enseña el sistema.
Durante un incidente, el tiempo es escaso y la confianza frágil. El código escrito con supuestos explícitos (precondiciones), cheques significativos y flujo de control directo es más fácil de rastrear bajo presión.
Igualmente importante: los cambios disciplinados son más fáciles de revertir. Ediciones más pequeñas y localizadas con fronteras claras reducen la posibilidad de que un rollback provoque fallos nuevos. El resultado no es perfección: son menos sorpresas, recuperaciones más rápidas y un sistema que se mantiene manejable a medida que pasan los años y se suman contribuyentes.
El punto de Dijkstra no era “programar a la antigua”. Era “escribir código que puedas explicar”. Puedes adoptar esa mentalidad sin convertir cada característica en un ejercicio de prueba formal.
Empieza con decisiones que abaraten el razonamiento:
Una buena heurística: si no puedes resumir en una frase lo que una función garantiza, probablemente hace demasiado.
No necesitas un gran sprint de refactor. Añade estructura en las costuras:
isEligibleForRefund).Estas mejoras son incrementales: reducen la carga cognitiva para el siguiente cambio.
Al revisar o escribir un cambio, pregunta:
Si los revisores no pueden responder rápido, el código está señalando dependencias ocultas.
Los comentarios que restan el código se vuelven obsoletos. En su lugar, escribe por qué el código es correcto: los supuestos clave, los casos límite que proteges y qué pasaría si esos supuestos cambian. Una nota corta como “Invariant: total is always the sum of processed items” puede valer más que un párrafo narrativo.
Si quieres un lugar ligero para capturar estos hábitos, recógelos en una lista compartida (ver /blog/practical-checklist-for-disciplined-code).
Los equipos modernos usan cada vez más IA para acelerar la entrega. El riesgo es conocido: velocidad hoy puede convertirse en confusión mañana si el código generado es difícil de explicar.
Una manera afín a Dijkstra de usar IA es tratarla como un acelerador del pensamiento estructurado, no como su reemplazo. Por ejemplo, al construir en Koder.ai—una plataforma vibe-coding donde creas apps web, backend y móviles por chat—puedes mantener los hábitos de “razonar primero” haciendo explícitos tus prompts y pasos de revisión:
Aunque eventualmente exportes el código y lo ejecutes en otro lado, la misma regla aplica: el código generado debe ser código que puedas explicar.
Esta es una checklist ligera “amigable a Dijkstra” que puedes usar en revisiones, refactors o antes de mergear. No se trata de escribir pruebas todo el día: se trata de hacer que la corrección y la claridad sean la opción por defecto.
total always equals sum of processed items” previene bugs sutiles.Elige un módulo desordenado y reestructura el flujo de control primero:
Luego añade algunas pruebas focalizadas alrededor de las nuevas fronteras. Si quieres más patrones como este, explora posts relacionados en /blog.
Porque cuando las bases de código crecen, el principal cuello de botella pasa a ser entenderlas, no escribirlas. El enfoque de Dijkstra en flujos de control predecibles, contratos claros y corrección reduce el riesgo de que un “cambio pequeño” provoque comportamientos sorprendentes en otras partes, que es precisamente lo que ralentiza a los equipos con el tiempo.
En este contexto, “escala” trata menos sobre rendimiento y más sobre la multiplicación de la complejidad:
Estas fuerzas hacen que razonar y prever el comportamiento valga mucho más que la astucia puntual.
La programación estructurada favorece un pequeño conjunto de estructuras de control claras:
if/else, switch)for, while)El objetivo no es rigidez, sino hacer que las rutas de ejecución sean fáciles de seguir para poder explicar el comportamiento, revisar cambios y depurar sin “teletransportarse” por el código.
El problema viene de los saltos sin restricciones que generan rutas de ejecución difíciles de predecir y estados poco claros. Cuando el control está enmarañado, los desarrolladores pierden tiempo preguntando cosas básicas como “¿Cómo hemos llegado aquí?” o “¿Qué estado es válido ahora?”.
Equivalentes modernos incluyen ramas profundamente anidadas, salidas tempranas dispersas y cambios de estado implícitos que hacen que el comportamiento sea difícil de rastrear.
La corrección es la “característica silenciosa” en la que confían los usuarios: el sistema hace consistentemente lo que promete y falla de maneras predecibles y explicables cuando no puede. Es la diferencia entre “funciona en algunos ejemplos” y “sigue funcionando tras refactors, integraciones y casos límite”.
Porque las dependencias amplifican los errores. Un estado incorrecto o un fallo de límite pequeño se copia, se cachea, se reintenta, se envuelve y se parchea en múltiples módulos y servicios. Con el tiempo, los equipos dejan de preguntarse “¿qué es verdad?” y empiezan a asumir “qué suele pasar”, lo que hace que los incidentes sean más complejos y los cambios más riesgosos.
Simplicidad significa pocas ideas en movimiento al mismo tiempo: responsabilidades claras, flujo de datos claro y mínimas excepciones. No es menos líneas ni one-liners ingeniosos.
Una buena prueba es si el comportamiento sigue siendo fácil de predecir cuando cambian los requisitos. Si cada nuevo caso añade reglas tipo “a menos que…”, estás acumulando complejidad accidental.
Un invariante es un hecho que debe seguir siendo verdadero durante la ejecución de un bucle o una transición de estado. Forma ligera de aplicarlo:
total equals the sum of processed items”)Esto hace que las ediciones posteriores sean más seguras porque la siguiente persona sabe qué no debe romperse.
Las pruebas descubren bugs probando ejemplos; el razonamiento previene categorías enteras de bugs volviendo la lógica explícita. Las pruebas no pueden demostrar la ausencia de defectos porque no cubren todos los inputs o variaciones temporales. El razonamiento es especialmente valioso en áreas de alto coste por fallo (dinero, seguridad, concurrencia).
Una mezcla práctica: pruebas extensas + aserciones focalizadas + precondiciones/postcondiciones claras alrededor de la lógica crítica.
Empieza con movimientos pequeños y repetibles que reduzcan la carga cognitiva:
Son “mejoras estructurales” incrementales que abaratan el siguiente cambio sin requerir una reescritura.