8 min

John Ousterhout: diseño práctico, Tcl y el coste de la complejidad

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: diseño práctico, Tcl y el coste de la complejidad

Por qué el mensaje de Ousterhout sigue importando

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 tema central: la complejidad gravita sobre todo

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.

Tres lentes que usaremos en este artículo

Para concretar la idea (y que no sea solo motivacional), veremos el mensaje de Ousterhout desde tres ángulos:

  • El legado de Tcl: qué acertó Tcl sobre simplicidad, composición y “pegamento”, y por qué esas ideas se difundieron más allá del lenguaje.
  • La conexión con Brooks: cómo se relaciona “No Silver Bullet” con la visión de Ousterhout, dónde coinciden y qué enseña la discrepancia a los equipos que intentan lanzar.
  • Reglas prácticas de diseño: especialmente “módulos profundos” y técnicas de diseño de API que reducen la carga cognitiva para la próxima persona que tenga que cambiar el sistema (que normalmente eres tú).

Qué puedes esperar aprender

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.

Qué significa realmente “complejidad” en equipos cotidianos

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.

Cómo se manifiesta la complejidad en el trabajo diario

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:

  • Dependencias ocultas: una función depende silenciosamente de una columna de base de datos, un job en segundo plano o una bandera de configuración que no es obvia desde el código que estás editando.
  • Casos especiales: “Excepto para clientes enterprise”, “Excepto cuando el usuario se registró antes de 2021”, “Excepto si la petición vino de móvil”. Esas excepciones se apilan hasta que el “camino normal” deja de ser claro.
  • Propiedad poco clara: nadie se siente responsable de un área, así que las correcciones se convierten en parches cautelosos en lugar de mejoras claras. Con el tiempo, la ruta más segura es “añadir otro workaround”.

El costo: velocidad, calidad y confianza

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.

Complejidad esencial vs accidental

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.

El legado de Tcl: las buenas ideas que se difundieron

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.

La idea del “lenguaje pegamento”

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”.

Lo que Tcl hizo bien (y lo que se extendió)

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.

Limitaciones y por qué perdió terreno

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.

Lecciones de diseño escondidas en la filosofía de Tcl

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.

Un núcleo pequeño que fomenta la composición

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.

“Fácil de usar” vs “fácil de implementar”

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?

Las primitivas pequeñas pueden calmar—o cortar

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.

Componibilidad vs funciones puntuales (no técnico)

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.

Brooks en una página: “No Silver Bullet” y su afirmación

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.

Complejidad esencial vs accidental

Brooks dividió la complejidad en dos cubos:

  • Complejidad esencial: la dificultad que proviene del problema mismo—las reglas del mundo real, casos límite y metas en conflicto que el software debe representar.
  • Complejidad accidental: la dificultad creada por nuestros métodos y herramientas—lenguajes incómodos, pipelines de compilación torpes, despliegues manuales o arquitecturas que te obligan a pensar en demasiados detalles a la vez.

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.

Por qué todavía importa

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?

El debate “Ousterhout vs Brooks”, sin calor

Compensa tu tiempo de desarrollo
Obtén créditos compartiendo tu proyecto o refiriendo a compañeros para que prueben Koder.ai.

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.

La réplica de Ousterhout: el diseño compra más de lo que crees

“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.

La advertencia de Brooks: parte de la complejidad está integrada

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.

Dónde están de acuerdo

Coinciden más de lo que parece:

  • Parte de la complejidad es inevitable porque el mundo es complicado.
  • Gran parte del dolor viene de la complejidad accidental—detalles y excepciones que no deberían existir.
  • El costo real aparece después: iteración más lenta, más riesgo y muchas áreas “no tocar”.

La pregunta práctica para los equipos

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 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.

Módulos profundos: ocultar la complejidad de la forma correcta

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…”.

Profundo vs superficial: una analogía

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.

Cuando añadir capas ayuda (y cuándo duele)

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.

Lista rápida: cómo detectar módulos superficiales

  • La API tiene muchos parámetros, especialmente booleanos como useCache, skipValidation, force, legacy.
  • Los llamantes deben seguir una secuencia específica (“llama A antes de B”) para evitar bugs sutiles.
  • El módulo filtra conceptos internos (rutas de archivo, nombres de tabla, reglas de hilos) en la interfaz.
  • La mayoría de los cambios requieren tocar muchos sitios porque la abstracción no estabiliza el comportamiento.
  • La documentación parece una etiqueta de advertencia más que una promesa (“No uses X cuando Y a menos que Z”).

Los módulos profundos no solo “encapsulan código”. Encapsulan decisiones.

Diseño de API que reduce la carga cognitiva

Comparte una demo realista
Presenta prototipos como productos reales con un dominio personalizado al compartirlos con las partes interesadas.

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.

Qué hace que una API sea amigable para humanos

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.

Por qué “más opciones” eleva los costes de todos

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.

Valores por defecto, convenciones y nombres

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).

Dónde se cuela la complejidad: arreglos tácticos y casos especiales

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.

Trampas comunes que se componen en silencio

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 vs estratégica (versión en lenguaje llano)

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.

Guardarraíles simples que realmente ayudan

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.

Por qué la complejidad mata productos, no solo codebases

La complejidad no se queda atrapada en ingeniería. Se filtra en cronogramas, fiabilidad y en cómo los clientes experimentan tu producto.

Costes a nivel producto: velocidad, estabilidad e incorporación

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.

La complejidad aparece como confusión en el cliente

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.

El impuesto de la complejidad en soporte y operaciones

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.

Un ejemplo práctico: una función más vs flujos más simples

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.

Cómo gestionar la complejidad como una restricción de producto de primera clase

Del concepto a la base de código
Convierte un flujo de producto claro en una app web en React con backend en Go.

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.

Pon un “presupuesto de complejidad” en la hoja de ruta

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.

Usa métricas ligeras que los equipos puedan mantener

No necesitas números perfectos, solo señales que vayan en la dirección correcta:

  • Área de superficie del módulo: número de métodos/endpoints públicos, flags o campos de configuración expuestos.
  • Conteo de conceptos: cuántas ideas debe aprender un usuario (o un ingeniero nuevo) para tener éxito.
  • Tasa de fallos de cambio: con qué frecuencia los despliegues o releases requieren rollback, hotfixes o trabajo urgente.

Haz seguimiento por release y relaciónalo con decisiones: “Añadimos dos opciones públicas; ¿qué quitamos o simplificamos para compensar?”

Prototipa para probar simplicidad, no solo factibilidad

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.

Programa limpiezas de complejidad con criterios claros de éxito

Haz que el trabajo de “limpieza de complejidad” sea periódico (trimestral o al menos en cada release mayor), y define qué significa “hecho”:

  • Eliminar una opción o caso especial (no solo refactorizar).
  • Reducir pasos de incorporación o configuración requerida.
  • Colapsar dos APIs solapadas en una.
  • Mejorar la tasa de fallos de cambio en un área objetivo.

El objetivo no es código más limpio en abstracto—es menos conceptos, menos excepciones y cambios más seguros.

Conclusiones prácticas para equipos este trimestre

Aquí hay algunas acciones que traducen la idea de Ousterhout “la complejidad es el enemigo” en hábitos semanales.

5–7 conclusiones contundentes

  • Trata la complejidad como un centro de costes: si no aporta valor al usuario, necesita aprobación presupuestaria.
  • Prefiere menos módulos profundos a muchas capas finas que filtran detalles.
  • Apunta a interfaces que se expliquen solas: buenos nombres, pequeña área de superficie, invariantes claras.
  • No “añadas otra opción”. Las opciones multiplican interacciones; los casos especiales se componen con el tiempo.
  • Si una corrección exige conocimiento del llamante, probablemente moviste la complejidad hacia afuera.
  • Haz de la eliminación una métrica de éxito: borrar código y casos es a menudo el trabajo de diseño con mayor apalancamiento.

Un plan corto de acción (1–2 semanas)

Elige un subsistema que cause confusión regularmente (dolor en la incorporación, bugs recurrentes, muchas preguntas “¿cómo funciona esto?”).

  1. Mapea la interfaz: lista funciones/endpoints públicos, flags de configuración y qué deben saber los llamantes.
  2. Simplifica el contrato: consolida parámetros, elimina banderas “modo” y escribe 2–3 invariantes que el módulo garantice.
  3. Elimina casos especiales: quita ramas añadidas para un cliente, un entorno o un bug histórico—luego reemplaza por una regla general.
  4. Añade una puerta ligera: nuevas flags y excepciones requieren una nota de diseño corta y un revisor que pregunte, “¿podemos evitar el caso especial?”

Lecturas y seguimientos

  • John Ousterhout, A Philosophy of Software Design
  • Fred Brooks, “No Silver Bullet”
  • Fred Brooks, The Mythical Man-Month (especialmente sobre integridad conceptual)

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?

Preguntas frecuentes

¿Qué significa “complejidad” en el trabajo diario de software?

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).

¿Cómo puede un equipo detectar la complejidad antes de que se convierta en crisis?

Busca señales de que razonar es caro:

  • El comportamiento depende de dependencias ocultas (una columna, un trabajo en segundo plano, una configuración o una caché que no imaginabas que importaba).
  • El “camino normal” no está claro por las excepciones acumuladas (“excepto enterprise”, “excepto cuentas antiguas”).
  • Los cambios requieren coordinación entre muchas personas/servicios para ser seguros.
  • La documentación y los comentarios parecen etiquetas de advertencia (“no llames a X cuando Y a menos que Z”).
¿Cuál es la diferencia entre complejidad esencial y accidental?

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.

¿Qué es un “módulo profundo” y por qué importa?

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.

¿Cómo reconocer un módulo superficial o una abstracción que filtra?

Síntomas comunes:

  • Muchos parámetros y booleanos (legacy, skipValidation, force, mode).
  • Orden de llamadas requerido («llama A antes de B») que la API no hace cumplir.
  • Conceptos internos se filtran en la interfaz (nombres de tablas, rutas de archivo, claves de caché).
  • Cambios pequeños se propagan a muchos puntos de llamada.

Los módulos superficiales suelen parecer organizados pero trasladan la complejidad a cada llamante.

¿Cuáles son reglas prácticas para diseñar APIs que reduzcan la carga cognitiva?

Prefiere APIs que sean:

  • Pequeñas y consistentes: pocas ideas que se combinan.
  • Difíciles de usar mal: validación en los límites, invariantes claras, valores por defecto seguros.
  • Bajas en combinatoria: evita explosiones de opciones donde las banderas interactúan de forma impredecible.

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.

¿Cómo deben gestionar los equipos los feature flags para que no creen complejidad permanente?

Usa flags para despliegues controlados, y trátalos como deuda con fecha de caducidad:

  • Añade un plan de eliminación al crear la bandera (propietario + fecha límite).
  • Poda flags obsoletas periódicamente; consolida las que se solapan.
  • Evita flags que cambien la semántica en muchos lugares: prefiere un único límite donde se toma la decisión.

Las banderas de larga duración multiplican el número de “sistemas” que los ingenieros deben razonar.

¿Qué significa poner un “presupuesto de complejidad” en la hoja de ruta?

Haz explícita la complejidad en la planificación, no solo en las revisiones de código:

  • Define una regla como “no conceptos nuevos netos a menos que eliminemos uno”.
  • Cobra más alcance a las features que introducen modos, configuraciones o casos especiales.
  • Mide señales simples por release (endpoints/opciones públicas añadidas, campos de configuración añadidos, tasa de fallos de cambios).

El objetivo es forzar los trade-offs antes de que la complejidad se institucionalice.

¿Cuál es la diferencia entre programación táctica y estratégica en la práctica?

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.

¿Qué pueden aprender los equipos modernos de la filosofía de Tcl sobre “lenguaje pegamento”?

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:

  • Sistemas de plugins/extensiones con primitivas anfitrión estables.
  • Capas de scripting o políticas para automatización (ops, QA, herramientas internas).
  • Lenguajes de configuración que mantienen el núcleo estable y permiten composición flexible.

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.

Related posts