Explora las ideas de John Ousterhout sobre diseño práctico de software, el legado de Tcl, el debate Ousterhout vs Brooks y cómo la complejidad hunde productos.

John Ousterhout es un científico de la computación e ingeniero cuyo trabajo abarca investigación y sistemas reales. Creó el lenguaje de programación Tcl, ayudó a moldear sistemas de archivos modernos y, más tarde, destiló décadas de experiencia en una afirmación simple y algo incómoda: la complejidad es el enemigo principal del software.
Ese mensaje sigue vigente porque la mayoría de los equipos no fallan por falta de funciones o esfuerzo: fallan porque sus sistemas (y organizaciones) se vuelven difíciles de entender, difíciles de cambiar y fáciles de romper. La complejidad no solo ralentiza a los ingenieros. Se filtra en decisiones de producto, confianza en la hoja de ruta, confianza del cliente, frecuencia de incidentes e incluso contratación, porque la incorporación se convierte en un proceso de meses.
El encuadre de Ousterhout es práctico: cuando un sistema acumula casos especiales, excepciones, dependencias ocultas y arreglos de “solo esta vez”, el costo no se limita al código. Todo el producto se vuelve más caro de evolucionar. Las funciones tardan más, QA se complica, los lanzamientos son más riesgosos y los equipos empiezan a evitar mejoras porque tocar cualquier cosa parece peligroso.
Esto no es un llamado a la pureza académica. Es un recordatorio de que cada atajo tiene pagos de intereses—y la complejidad es la deuda con el interés más alto.
Para concretar la idea (y que no sea solo motivacional), veremos el mensaje de Ousterhout desde tres ángulos:
No está escrito solo para fanáticos de lenguajes. Si construyes productos, lideras equipos o tomas decisiones de hoja de ruta, encontrarás formas accionables de detectar la complejidad temprano, evitar que se institucionalice y tratar la simplicidad como una restricción de primera clase, no como un extra deseable después del lanzamiento.
La complejidad no es “mucho código” o “matemáticas difíciles”. Es la brecha entre lo que crees que hará el sistema cuando lo cambies y lo que realmente hace. Un sistema es complejo cuando las ediciones pequeñas parecen arriesgadas, porque no puedes predecir el radio de impacto.
En código sano, puedes responder: “Si cambiamos esto, ¿qué más podría romper?” La complejidad es lo que hace que esa pregunta sea cara.
A menudo se esconde en:
Los equipos sienten la complejidad como despliegues más lentos (más tiempo investigando), más errores (porque el comportamiento sorprende) y sistemas frágiles (los cambios requieren coordinación entre muchas personas y servicios). También penaliza la incorporación: los nuevos integrantes no pueden construir un modelo mental, así que evitan tocar los flujos centrales.
Parte de la complejidad es esencial: reglas del negocio, requisitos normativos, casos límite del mundo real. No puedes eliminar eso.
Pero mucha es accidental: APIs confusas, lógica duplicada, banderas “temporales” que se vuelven permanentes y módulos que filtran detalles internos. Esa es la complejidad que las decisiones de diseño crean—y la única que puedes pagar sistemáticamente.
Tcl nació con un objetivo práctico: facilitar la automatización del software y extender aplicaciones existentes sin reescribirlas. John Ousterhout lo diseñó para que los equipos pudieran añadir “la cantidad justa de programabilidad” a una herramienta y luego ceder ese poder a usuarios, operadores, QA o cualquiera que necesitara scriptar flujos.
Tcl popularizó la noción de un lenguaje de pegamento: una capa de scripting pequeña y flexible que conecta componentes escritos en lenguajes más rápidos y de bajo nivel. En lugar de construir cada función dentro de un monolito, podías exponer un conjunto de comandos y componerlos en nuevos comportamientos.
Ese modelo fue influyente porque coincidía con cómo ocurre el trabajo. La gente no solo construye productos; construye sistemas de compilación, harnesses de pruebas, herramientas de administración, convertidores de datos y automatizaciones puntuales. Una capa ligera de scripting convierte esas tareas de “abrir un ticket” a “escribir un script”.
Tcl hizo de la embebición una preocupación de primera clase. Podías insertar un intérprete en una aplicación, exportar una interfaz de comandos limpia y ganar inmediatamente configurabilidad y iteración rápida.
Ese patrón aparece hoy en sistemas de plugins, lenguajes de configuración, APIs de extensión y runtimes de scripting embebidos—aunque la sintaxis del script no sea igual a la de Tcl.
También fomentó un hábito de diseño importante: separar primitivas estables (las capacidades centrales de la app anfitriona) de la composición cambiante (scripts). Cuando funciona, las herramientas evolucionan más rápido sin desestabilizar constantemente el núcleo.
La sintaxis de Tcl y su modelo de “todo es una cadena” podían resultar poco intuitivos, y las bases de código grandes en Tcl a veces eran difíciles de razonar sin convenciones estrictas. A medida que los ecosistemas ofrecieron bibliotecas estándar más ricas, mejor tooling y comunidades mayores, muchos equipos migraron.
Nada de eso borra el legado de Tcl: normalizó la idea de que extensibilidad y automatización no son extras, son características de producto que pueden reducir drásticamente la complejidad para quienes usan y mantienen un sistema.
Tcl se construyó alrededor de una idea aparentemente estricta: mantener el núcleo pequeño, hacer la composición poderosa y mantener los scripts lo bastante legibles para que la gente trabaje sin traducciones constantes.
En lugar de enviar un gran conjunto de funciones especializadas, Tcl confiaba en un conjunto compacto de primitivas (cadenas, comandos, reglas simples de evaluación) y esperaba que los usuarios las combinaran.
Esa filosofía empuja a los diseñadores hacia menos conceptos, reutilizados en muchos contextos. La lección para producto y diseño de API es clara: si puedes resolver diez necesidades con dos o tres bloques de construcción consistentes, reduces la superficie que la gente debe aprender.
Una trampa clave es optimizar para la conveniencia del desarrollador. Una función puede ser fácil de implementar (copiar una opción existente, añadir una bandera especial, parchear un caso límite) y a la vez hacer el producto más difícil de usar.
Tcl enfatizaba lo contrario: mantener el modelo mental estrecho, incluso si la implementación tiene que hacer más trabajo internamente.
Cuando revises una propuesta, pregunta: ¿esto reduce el número de conceptos que el usuario debe recordar, o añade una excepción más?
El minimalismo solo ayuda cuando las primitivas son consistentes. Si dos comandos parecen similares pero se comportan distinto en casos límite, los usuarios acaban memorizando trivialidades. Un conjunto pequeño de herramientas puede volverse “filoso” cuando las reglas varían sutilmente.
Piensa en una cocina: un buen cuchillo, una sartén y un horno te permiten preparar muchas comidas combinando técnicas. Un gadget que solo corta aguacates es una función puntual—fácil de vender, pero que ensucia los cajones.
La filosofía de Tcl aboga por el cuchillo y la sartén: herramientas generales que se componen limpiamente, para que no necesites un gadget nuevo para cada receta.
En 1986, Fred Brooks escribió un ensayo con una conclusión intencionalmente provocadora: no existe un único avance—ninguna “bala de plata”—que haga el desarrollo de software un orden de magnitud más rápido, barato y fiable de un salto.
Su punto no era que el progreso sea imposible. Era que el software ya es un medio donde podemos hacer casi cualquier cosa, y esa libertad trae una carga única: definimos la cosa mientras la construimos. Mejores herramientas ayudan, pero no borran la parte más dura del trabajo.
Brooks dividió la complejidad en dos cubos:
Las herramientas pueden aplastar la complejidad accidental. Piensa en lo que ganamos con lenguajes de alto nivel, control de versiones, CI, contenedores, bases de datos gestionadas y buenos IDEs. Pero Brooks argumentó que la complejidad esencial domina y no desaparece solo porque mejore el tooling.
Incluso con plataformas modernas, los equipos siguen gastando la mayor parte de su energía negociando requisitos, integrando sistemas, manejando excepciones y manteniendo el comportamiento consistente en el tiempo. La superficie puede cambiar (APIs cloud en lugar de drivers de dispositivo), pero el desafío central sigue ahí: traducir necesidades humanas en comportamientos precisos y mantenibles.
Esto crea la tensión en la que se apoya Ousterhout: si la complejidad esencial no se puede eliminar, ¿puede el diseño disciplinado reducir significativamente cuánto de ella se filtra al código—y a las cabezas de los desarrolladores en el día a día?
La gente a veces enmarca “Ousterhout vs Brooks” como una pelea entre optimismo y realismo. Es más útil leerlo como dos ingenieros experimentados describiendo partes diferentes del mismo problema.
“No Silver Bullet” de Brooks dice que no hay un único avance que borre la parte difícil del software. Ousterhout no lo discute realmente.
Su réplica es más estrecha y práctica: los equipos a menudo tratan la complejidad como inevitable cuando mucha de ella es autoinfligida.
En la visión de Ousterhout, el buen diseño puede reducir la complejidad de forma significativa—no haciendo el software “fácil”, sino haciéndolo menos confuso de cambiar. Es una afirmación importante porque la confusión es lo que convierte el trabajo diario en trabajo lento.
Brooks se centra en la dificultad esencial: el software debe modelar realidades desordenadas, requisitos cambiantes y casos límite que están fuera del código. Incluso con herramientas excelentes, no puedes borrar eso. Solo puedes gestionarlo.
Coinciden más de lo que parece:
En lugar de preguntar “¿Quién tiene razón?”, pregunta: ¿Qué complejidad podemos controlar este trimestre?
Los equipos no controlan cambios de mercado ni la dificultad intrínseca del dominio. Pero sí pueden controlar si las nuevas funciones añaden casos especiales, si las APIs obligan a los llamantes a recordar reglas ocultas y si los módulos ocultan o filtran complejidad.
Ese es el terreno de acción: aceptar la complejidad esencial y ser implacables con la accidental.
Un módulo profundo es un componente que hace mucho, exponiendo una interfaz pequeña y fácil de entender. La “profundidad” es la cantidad de complejidad que el módulo se quita de encima: los llamantes no necesitan conocer los detalles confusos y la interfaz no los obliga a ello.
Un módulo superficial es lo contrario: puede envolver una pequeña lógica, pero empuja la complejidad hacia afuera—mediante muchos parámetros, banderas especiales, orden de llamadas requerido o reglas de “tienes que recordar…”.
Piensa en un restaurante. Un módulo profundo es la cocina: pides “pasta” desde un menú simple y no te preocupas por proveedores, tiempos de hervido o emplatado.
Un módulo superficial es una “cocina” que te da ingredientes crudos con una hoja de instrucciones de 12 pasos y te pide traer tu propia sartén. El trabajo sigue ocurriendo—pero lo movieron al cliente.
Las capas adicionales son útiles si colapsan muchas decisiones en una elección obvia.
Por ejemplo, una capa de almacenamiento que expone save(order) y maneja reintentos, serialización e indexado internamente es profunda.
Las capas dañan cuando solo renombrar cosas o añadir opciones. Si una nueva abstracción introduce más configuración de la que elimina—por ejemplo save(order, format, retries, timeout, mode, legacyMode)—probablemente es superficial. El código puede parecer “organizado”, pero la carga cognitiva aparece en cada sitio de llamada.
useCache, skipValidation, force, legacy.Los módulos profundos no solo “encapsulan código”. Encapsulan decisiones.
Una API “buena” no es solo la que puede hacer mucho. Es la que la gente puede sostener en la cabeza mientras trabaja.
La lente de diseño de Ousterhout te empuja a juzgar una API por el esfuerzo mental que exige: cuántas reglas debes recordar, cuántas excepciones predecir y qué tan fácil es equivocarse.
Las APIs orientadas a humanos suelen ser pequeñas, consistentes y difíciles de usar mal.
Pequeña no significa poco potente: significa que la superficie se concentra en unos pocos conceptos que se componen bien. Consistente significa que el mismo patrón funciona en todo el sistema (parámetros, manejo de errores, nombres, tipos de retorno). Difícil de usar mal significa que la API guía hacia rutas seguras: invariantes claras, validación en los límites y chequeos de tipo o en tiempo de ejecución que fallen pronto.
Cada bandera, modo u “opción por si acaso” es un impuesto para todos los usuarios. Incluso si solo el 5% de los llamantes la necesita, el 100% ahora debe saber que existe, preguntarse si la necesita e interpretar el comportamiento cuando interactúa con otras opciones.
Así es como las APIs acumulan complejidad oculta: no en una sola llamada, sino en la combinatoria.
Los valores por defecto son una amabilidad: permiten que la mayoría de los llamantes omitan decisiones y aun así obtengan un comportamiento sensato. Las convenciones (una forma obvia de hacerlo) reducen la ramificación en la mente del usuario. El nombrado hace trabajo real: elige verbos y sustantivos que coincidan con la intención del usuario y mantén operaciones similares nombradas de forma parecida.
Un recordatorio más: las APIs internas importan tanto como las públicas. La mayor parte de la complejidad en productos vive detrás de escena—límites de servicio, librerías compartidas y módulos “helper”. Trata esas interfaces como productos, con revisiones y disciplina de versionado (véase también /blog/deep-modules).
La complejidad rara vez llega como una única “mala decisión”. Se acumula a través de parches pequeños y razonables—especialmente cuando los equipos están bajo presión y el objetivo inmediato es lanzar.
Una trampa es banderas de características por todas partes. Las flags son útiles para despliegues seguros, pero cuando perduran, cada flag multiplica el número de comportamientos posibles. Los ingenieros dejan de razonar sobre “el sistema” y empiezan a razonar sobre “el sistema, excepto cuando la flag A está activa y el usuario está en el segmento B”.
Otra es la lógica de caso especial: “Los clientes enterprise necesitan X”, “Excepto en la región Y”, “A menos que la cuenta tenga más de 90 días”. Esas excepciones suelen esparcirse por la base de código y, tras unos meses, nadie sabe cuáles siguen siendo necesarias.
Una tercera es abstracciones que filtran. Una API que obliga a los llamantes a entender detalles internos (temporización, formato de almacenamiento, reglas de caché) empuja la complejidad hacia afuera. En lugar de que un módulo cargue con la complejidad, cada llamante aprende las rarezas.
Programación táctica optimiza esta semana: arreglos rápidos, cambios mínimos, “parchealo”.
Programación estratégica optimiza el próximo año: pequeños rediseños que previenen la misma clase de bugs y reducen trabajo futuro.
El peligro es el “interés de mantenimiento”. Un workaround rápido parece barato ahora, pero lo pagas con intereses: incorporación más lenta, lanzamientos frágiles y desarrollo guiado por el miedo donde nadie quiere tocar el código antiguo.
Añade recordatorios ligeros en las revisiones de código: “¿Esto añade un nuevo caso especial?” “¿Puede la API ocultar este detalle?” “¿Qué complejidad dejamos atrás?”
Mantén registros breves de decisiones para trade-offs no triviales (unos pocos puntos bastan). Y reserva un pequeño presupuesto de refactor en cada sprint para que las correcciones estratégicas no se traten como trabajo extracurricular.
La complejidad no se queda atrapada en ingeniería. Se filtra en cronogramas, fiabilidad y en cómo los clientes experimentan tu producto.
Cuando un sistema es difícil de entender, cada cambio lleva más tiempo. El time-to-market se retrasa porque cada release requiere más coordinación, más testing de regresión y más ciclos “por si acaso”.
La fiabilidad también sufre. Los sistemas complejos crean interacciones que nadie puede predecir por completo, así que los bugs aparecen como casos límite: el checkout falla solo cuando un cupón, un carrito guardado y una regla de impuestos regional se combinan de una forma concreta. Esos incidentes son los más difíciles de reproducir y los más lentos de arreglar.
La incorporación se vuelve un lastre oculto. Los nuevos miembros no pueden construir un modelo mental útil, así que evitan áreas riesgosas, copian patrones que no entienden y añaden sin querer más complejidad.
A los clientes no les importa si un comportamiento viene de un “caso especial” en el código. Lo experimentan como inconsistencia: ajustes que no aplican en todas partes, flujos que cambian según por dónde llegaste, funciones que funcionan “la mayoría de las veces”.
La confianza cae, la rotación sube y la adopción se estanca.
Soporte paga la complejidad con tickets más largos y más idas y venidas para recopilar contexto. Operaciones paga con más alertas, más runbooks y despliegues más cautelosos. Cada excepción se convierte en algo que monitorizar, documentar y explicar.
Imagina solicitudes para “una regla de notificaciones más”. Añadirla parece rápido, pero introduce otra rama en el comportamiento, más copy en la UI, más casos de prueba y más formas en que los usuarios pueden configurar mal las cosas.
Ahora compara eso con simplificar el flujo de notificaciones existente: menos tipos de regla, valores por defecto más claros y comportamiento consistente entre web y móvil. Puede que lances menos perillas, pero reduces sorpresas—haciendo el producto más fácil de usar, más fácil de soportar y más rápido de evolucionar.
Trata la complejidad como el rendimiento o la seguridad: algo para planificar, medir y proteger. Si solo notas la complejidad cuando la entrega se ralentiza, ya estás pagando intereses.
Junto con el alcance de funcionalidades, define cuánto nueva complejidad puede introducir una release. El presupuesto puede ser simple: “sin conceptos nuevos netos a menos que quitemos uno” o “cualquier integración nueva debe reemplazar una vía antigua”.
Haz los trade-offs explícitos en la planificación: si una feature requiere tres nuevos modos de configuración y dos casos excepcionales, eso debería “costar” más que una feature que encaja en los conceptos existentes.
No necesitas números perfectos, solo señales que vayan en la dirección correcta:
Haz seguimiento por release y relaciónalo con decisiones: “Añadimos dos opciones públicas; ¿qué quitamos o simplificamos para compensar?”
Los prototipos a menudo se juzgan por “¿Podemos construirlo?” En su lugar, úsalos para responder: “¿Se siente simple de usar y difícil de usar mal?”
Haz que alguien ajeno a la feature intente una tarea realista con el prototipo. Mide tiempo hasta el éxito, preguntas realizadas y dónde hacen suposiciones erróneas. Esos son puntos calientes de complejidad.
Aquí es donde los flujos modernos pueden reducir la complejidad accidental—si mantienen la iteración cerrada y permiten deshacer errores. Por ejemplo, cuando los equipos usan una plataforma de prototipado como Koder.ai para bosquejar una herramienta interna o un nuevo flujo vía chat, funciones como planning mode (para aclarar la intención antes de generar) y snapshots/rollback (para deshacer cambios arriesgados rápidamente) pueden hacer que la experimentación temprana sea más segura—sin comprometerse con un montón de abstracciones a medio terminar. Si el prototipo progresa, aún puedes exportar el código fuente y aplicar la disciplina de “módulos profundos” y diseño de API descrita arriba.
Haz que el trabajo de “limpieza de complejidad” sea periódico (trimestral o al menos en cada release mayor), y define qué significa “hecho”:
El objetivo no es código más limpio en abstracto—es menos conceptos, menos excepciones y cambios más seguros.
Aquí hay algunas acciones que traducen la idea de Ousterhout “la complejidad es el enemigo” en hábitos semanales.
Elige un subsistema que cause confusión regularmente (dolor en la incorporación, bugs recurrentes, muchas preguntas “¿cómo funciona esto?”).
Seguimientos internos que puedes ejecutar: una “revisión de complejidad” en la planificación (/blog/complexity-review) y una comprobación rápida sobre si tu tooling está reduciendo complejidad accidental en lugar de añadir capas (/pricing).
¿Qué pieza de complejidad eliminarías primero si solo pudieras borrar un caso especial esta semana?
La complejidad es la brecha entre lo que esperas que ocurra cuando cambias el sistema y lo que realmente ocurre.
La sientes cuando las ediciones pequeñas parecen arriesgadas porque no puedes predecir el radio de impacto (tests, servicios, configuraciones, clientes o casos límite que podrías romper).
Busca señales de que razonar es caro:
La complejidad esencial proviene del dominio (regulaciones, casos límite del mundo real, reglas de negocio). No puedes eliminarla: solo modelarla bien.
La complejidad accidental se autoinflige (abstracciones que filtran, lógica duplicada, demasiados modos/banderas, APIs confusas). Esta es la parte que los equipos pueden reducir de forma fiable mediante diseño y simplificación.
Un módulo profundo hace mucho trabajo y expone una interfaz pequeña y estable. ‘Absorbe’ los detalles confusos (reintentos, formatos, ordenamientos, invariantes) para que los llamantes no tengan que conocerlos.
Prueba práctica: si la mayoría de los llamantes puede usar el módulo correctamente sin conocer las reglas internas, es profundo; si necesitan memorizar reglas y secuencias, es superficial.
Síntomas comunes:
legacy, skipValidation, force, mode).Prefiere APIs que sean:
Antes de añadir “una opción más”, pregúntate si puedes rediseñar la interfaz para que la mayoría de los llamantes no tenga que pensar en esa decisión.
Usa flags para despliegues controlados, y trátalos como deuda con fecha de caducidad:
Las banderas de larga duración multiplican el número de “sistemas” que los ingenieros deben razonar.
Haz explícita la complejidad en la planificación, no solo en las revisiones de código:
El objetivo es forzar los trade-offs antes de que la complejidad se institucionalice.
La programación táctica optimiza esta semana: parches rápidos, cambio mínimo, “sacarlo”.
La programación estratégica optimiza el próximo año: pequeños rediseños que eliminan clases recurrentes de errores y reducen el trabajo futuro.
Una heurística útil: si una solución requiere conocimiento del llamante (“recuerda llamar X primero” o “pon esta flag solo en prod”), probablemente necesites un cambio más estratégico para ocultar esa complejidad dentro del módulo.
La lección duradera de Tcl es el poder de un pequeño conjunto de primitivas más una composición fuerte, a menudo como una capa embebida de “pegamento”.
Equivalentes modernos:
El objetivo de diseño es el mismo: mantener el núcleo simple y estable, y permitir que el cambio ocurra a través de interfaces limpias.
Los módulos superficiales suelen parecer organizados pero trasladan la complejidad a cada llamante.