Explore as ideias de John Ousterhout sobre design prático de software, o legado do Tcl, o debate Ousterhout vs Brooks e como a complexidade afunda produtos.

John Ousterhout é um cientista da computação e engenheiro cujo trabalho atravessa pesquisa e sistemas reais. Ele criou a linguagem de programação Tcl, ajudou a moldar sistemas de arquivos modernos e mais tarde condensou décadas de experiência em uma afirmação simples e um pouco desconfortável: a complexidade é a inimiga principal do software.
Essa mensagem permanece atual porque a maioria das equipes não falha por falta de recursos ou esforço—elas fracassam porque seus sistemas (e organizações) ficam difíceis de entender, difíceis de mudar e fáceis de quebrar. A complexidade não só desacelera engenheiros. Ela vaza para decisões de produto, confiança no roadmap, confiança do cliente, frequência de incidentes e até contratação—porque onboarding vira um processo de meses.
O enquadramento de Ousterhout é prático: quando um sistema acumula casos especiais, exceções, dependências ocultas e correções “só desta vez”, o custo não se limita à base de código. O produto inteiro fica mais caro de evoluir. Recursos demoram mais, QA fica mais difícil, releases se tornam mais arriscados, e equipes começam a evitar melhorias porque tocar em qualquer coisa parece perigoso.
Isso não é um apelo por pureza acadêmica. É um lembrete de que todo atalho tem juros—e a complexidade é a dívida com maior taxa de juros.
Para tornar a ideia concreta (e não apenas motivacional), veremos a mensagem de Ousterhout por três ângulos:
Isto não foi escrito só para aficionados por linguagens. Se você constrói produtos, lidera equipes ou decide trade-offs no roadmap, encontrará formas acionáveis de identificar complexidade cedo, evitar que ela se torne institucionalizada e tratar simplicidade como uma restrição de primeira classe—não um luxo pós-lançamento.
Complexidade não é “muito código” ou “matemática difícil.” É a lacuna entre o que você acha que o sistema fará quando você mexer nele e o que ele realmente faz. Um sistema é complexo quando pequenas edições parecem arriscadas—porque você não consegue prever o raio de impacto.
Em código saudável, você consegue responder: “Se mudarmos isto, o que mais pode quebrar?” Complexidade é o que torna essa pergunta cara.
Frequentemente ela se esconde em:
Equipes sentem a complexidade como entregas mais lentas (mais tempo investigando), mais bugs (porque o comportamento surpreende) e sistemas frágeis (mudanças exigem coordenação entre muitas pessoas e serviços). Também pesa no onboarding: novos colegas não conseguem formar um modelo mental, então evitam tocar fluxos centrais.
Alguma complexidade é essencial: regras de negócio, requisitos de compliance, casos de borda do mundo real. Você não pode apagar isso.
Mas muita é acidental: APIs confusas, lógica duplicada, flags “temporárias” que viram permanentes e módulos que vazam detalhes internos. Essa é a complexidade que escolhas de design criam—e a única que você pode reduzir consistentemente.
O Tcl nasceu com um objetivo prático: facilitar automações e estender aplicações existentes sem reescrevê-las. John Ousterhout o desenhou para que equipes pudessem adicionar “programabilidade suficiente” a uma ferramenta—e então passar esse poder a usuários, operadores, QA ou qualquer pessoa que precisasse scriptar fluxos de trabalho.
Tcl popularizou a noção de linguagem de cola: uma camada de script pequena e flexível que conecta componentes escritos em linguagens mais rápidas e de nível mais baixo. Em vez de construir cada recurso em um monólito, você podia expor um conjunto de comandos e depois compô-los em novos comportamentos.
Esse modelo foi influente porque combinava com como o trabalho acontece na prática. Pessoas não só constroem produtos; constroem sistemas de build, harnesses de teste, ferramentas administrativas, conversores de dados e automações pontuais. Uma camada de script leve transforma essas tarefas de “abrir um ticket” em “escrever um script”.
O Tcl tornou o embedding uma preocupação de primeira classe. Você podia inserir um interpretador numa aplicação, exportar uma interface de comando limpa e ganhar configurabilidade e iteração rápida.
O mesmo padrão aparece hoje em sistemas de plugin, linguagens de configuração, APIs de extensão e runtimes de script embutidos—seja a sintaxe parecida com Tcl ou não.
Também reforçou um hábito de design importante: separar primitivas estáveis (capacidades centrais do host) de composição mutável (scripts). Quando funciona, ferramentas evoluem mais rápido sem desestabilizar constantemente o núcleo.
A sintaxe do Tcl e o modelo “tudo é string” podiam parecer contraintuitivos, e bases grandes de código Tcl às vezes ficavam difíceis de entender sem convenções fortes. À medida que novos ecossistemas ofereceram bibliotecas padrão mais ricas, melhores ferramentas e comunidades maiores, muitas equipes migraram naturalmente.
Nada disso apaga o legado do Tcl: ele ajudou a normalizar a ideia de que extensibilidade e automação não são extras—são recursos do produto que podem reduzir drasticamente a complexidade para quem usa e mantém um sistema.
O Tcl foi construído em torno de uma ideia aparentemente rígida: manter o núcleo pequeno, tornar a composição poderosa e manter scripts legíveis o suficiente para que pessoas trabalhem juntas sem tradução constante.
Em vez de enviar um grande conjunto de recursos especializados, o Tcl apoiou-se num conjunto compacto de primitivas (strings, comandos, regras simples de avaliação) e esperou que os usuários combinassem essas peças.
Essa filosofia empurra designers a menos conceitos, reutilizados em muitos contextos. A lição para produto e design de API é direta: se você pode resolver dez necessidades com dois ou três blocos de construção consistentes, você reduz a superfície que as pessoas precisam aprender.
Uma armadilha chave no design de software é otimizar para a conveniência de quem constrói. Um recurso pode ser fácil de implementar (copiar uma opção existente, adicionar uma flag especial, contornar um canto), enquanto torna o produto mais difícil de usar.
A ênfase do Tcl era o oposto: mantenha o modelo mental enxuto, mesmo que a implementação tenha que fazer mais trabalho nos bastidores.
Ao revisar uma proposta, pergunte: isso reduz o número de conceitos que um usuário precisa lembrar, ou adiciona mais uma exceção?
O minimalismo só ajuda quando as primitivas são consistentes. Se dois comandos parecem similares mas se comportam diferente em casos de borda, usuários acabam memorizando trivia. Um conjunto pequeno de ferramentas pode se tornar “arestas cortantes” quando regras variam sutilmente.
Pense numa cozinha: uma boa faca, uma frigideira e um forno permitem fazer muitas refeições combinando técnicas. Um aparelho que só corta abacates é um recurso pontual—fácil de vender, mas que entope gavetas.
A filosofia do Tcl defende a faca e a frigideira: ferramentas gerais que se combinam bem, para que você não precise de um novo gadget para cada receita.
Em 1986, Fred Brooks escreveu um ensaio com uma conclusão intencionalmente provocativa: não existe um único avanço—nenhuma “bala de prata”—que tornará o desenvolvimento de software uma ordem de magnitude mais rápido, barato e confiável num único salto.
Seu ponto não era que o progresso é impossível. Era que o software já é um meio em que podemos fazer quase qualquer coisa, e essa liberdade traz um ônus único: estamos constantemente definindo a coisa enquanto a construímos. Ferramentas melhores ajudam, mas não apagam a parte mais difícil do trabalho.
Brooks dividiu a complexidade em dois baldes:
Ferramentas podem esmagar a complexidade acidental. Pense no que ganhamos com linguagens de alto nível, controle de versão, CI, containers, bancos gerenciados e bons IDEs. Mas Brooks argumentou que a complexidade essencial domina, e ela não desaparece só porque a ferramenta melhora.
Mesmo com plataformas modernas, equipes ainda gastam a maior parte da energia negociando requisitos, integrando sistemas, lidando com exceções e mantendo comportamento consistente ao longo do tempo. A superfície pode mudar (APIs de cloud em vez de drivers de dispositivo), mas o desafio central permanece: traduzir necessidades humanas em comportamento preciso e mantível.
Isso cria a tensão que Ousterhout enfatiza: se a complexidade essencial não pode ser eliminada, um design disciplinado pode reduzir de forma significativa quanto dela vaza para o código—e para a cabeça dos desenvolvedores no dia a dia?
As pessoas às vezes enquadram “Ousterhout vs Brooks” como uma briga entre otimismo e realismo. É mais útil ler como dois engenheiros experientes descrevendo partes diferentes do mesmo problema.
O argumento de Brooks em “No Silver Bullet” diz que não há um salto único que remova a parte difícil do software. Ousterhout não discorda disso.
Sua reação é mais estreita e prática: equipes frequentemente tratam a complexidade como inevitável quando muita dela é autocriada.
Na visão de Ousterhout, bom design pode reduzir a complexidade de forma significativa—não tornando o software “fácil”, mas tornando-o menos confuso para mudar. Essa é uma afirmação importante, porque a confusão é o que transforma trabalho cotidiano em trabalho lento.
Brooks foca naquilo que chama de dificuldade essencial: o software precisa modelar realidades bagunçadas, requisitos mutáveis e casos de borda que existem fora da base de código. Mesmo com ótimas ferramentas e pessoas inteligentes, você não apaga isso. Só dá para gerenciá-lo.
Eles se sobrepõem mais do que o debate sugere:
Em vez de perguntar “Quem está certo?”, pergunte: Qual complexidade podemos controlar neste trimestre?
Equipes não controlam mudanças de mercado ou a dificuldade central do domínio. Mas podem controlar se novos recursos adicionam casos especiais, se APIs forçam chamadores a lembrar regras ocultas e se módulos escondem ou vazam complexidade.
Esse é o meio termo acionável: aceite a complexidade essencial e seja implacavelmente seletivo quanto à acidental.
Um módulo profundo é um componente que faz muito, expondo uma interface pequena e fácil de entender. A “profundidade” é a quantidade de complexidade que o módulo tira do seu colo: os chamadores não precisam conhecer os detalhes, e a interface não os obriga a isso.
Um módulo raso é o oposto: pode embrulhar um pequeno pedaço de lógica, mas empurra a complexidade para fora—por muitos parâmetros, flags especiais, ordem de chamadas exigida ou regras de “você precisa lembrar de…”.
Pense num restaurante. Um módulo profundo é a cozinha: você pede “massa” num menu simples e não se importa com escolhas de fornecedor, tempos de fervura ou montagem do prato.
Um módulo raso é uma “cozinha” que te entrega ingredientes crus com um manual de 12 passos e pede que você traga sua própria frigideira. O trabalho ainda acontece—mas foi deslocado para o cliente.
Camadas extras podem ser ótimas se elas colapsam muitas decisões em uma escolha óbvia.
Por exemplo, uma camada de armazenamento que expõe save(order) e cuida internamente de retries, serialização e indexação é profunda.
Camadas prejudicam quando basicamente renomeiam coisas ou adicionam opções. Se uma nova abstração introduz mais configuração do que remove—digamos, save(order, format, retries, timeout, mode, legacyMode)—ela provavelmente é rasa. O código pode ficar “organizado”, mas a carga cognitiva aparece em cada ponto de chamada.
useCache, skipValidation, force, legacy.Módulos profundos não apenas “encapsulam código”. Eles encapsulam decisões.
Uma API “boa” não é só capaz de fazer muito. É aquela que as pessoas conseguem manter na cabeça enquanto trabalham.
A lente de design de Ousterhout te pede avaliar a API pelo esforço mental que ela demanda: quantas regras você precisa lembrar, quantas exceções prever e quão fácil é fazer a coisa errada por acidente.
APIs humanas tendem a ser pequenas, consistentes e difíceis de usar errado.
Pequeno não significa subutilizado—significa que a superfície está concentrada em poucos conceitos que se combinam bem. Consistente significa que o mesmo padrão funciona em todo o sistema (parâmetros, tratamento de erros, nomenclatura, tipos de retorno). Difícil de usar errado significa que a API guia você para caminhos seguros: invariantes claras, validação nas fronteiras e checagens que falham cedo.
Cada flag extra, modo ou configuração “só por precaução” vira um imposto sobre todos os usuários. Mesmo se só 5% dos chamadores precisarem, 100% agora precisam saber que a opção existe, questionar se precisam dela e interpretar o comportamento quando ela interage com outras opções.
É assim que APIs acumulam complexidade oculta: não em uma única chamada, mas na combinatória.
Defaults são uma gentileza: permitem que a maioria dos chamadores omita decisões e ainda obtenha comportamento sensato. Convenções (uma forma óbvia de fazer) reduzem ramificações na mente do usuário. Nomes também fazem trabalho real: escolha verbos e substantivos que casem com a intenção do usuário e mantenha operações similares com nomes similares.
Mais um lembrete: APIs internas importam tanto quanto as públicas. A maior parte da complexidade dos produtos vive nos bastidores—fronteiras entre serviços, bibliotecas compartilhadas e módulos “helper”. Trate essas interfaces como produtos, com revisões e disciplina de versionamento (veja também /blog/deep-modules).
A complexidade raramente chega como uma única “decisão ruim”. Ela se acumula através de pequenos patches com aparência razoável—especialmente quando equipes estão sob pressão de prazo e o objetivo imediato é entregar.
Uma armadilha é feature flags por todo lado. Flags são úteis para rollouts seguros, mas quando ficam por aí, cada flag multiplica o número de comportamentos possíveis. Engenheiros deixam de raciocinar sobre “o sistema” e passam a raciocinar sobre “o sistema, exceto quando a flag A está ligada e o usuário está no segmento B”.
Outra é lógica de caso especial: “Clientes enterprise precisam de X”, “Exceto na região Y”, “A menos que a conta tenha mais de 90 dias”. Essas exceções costumam se espalhar pelo código, e depois de alguns meses ninguém sabe quais ainda são necessárias.
Uma terceira é abstrações que vazam. Uma API que força chamadores a entender detalhes internos (timing, formato de armazenamento, regras de cache) empurra a complexidade para fora. Em vez de um módulo carregar o peso, todo chamador aprende as peculiaridades.
Programação tática otimiza para esta semana: correções rápidas, mudanças mínimas, “aplique o patch”.
Programação estratégica otimiza para o próximo ano: pequenos redesenhos que previnem a mesma classe de bugs e reduzem trabalho futuro.
O perigo é o “juros de manutenção”. Um workaround rápido custa pouco agora, mas você o paga com juros: onboarding mais lento, releases frágeis e desenvolvimento guiado pelo medo, em que ninguém quer tocar o código antigo.
Adicione prompts leves na revisão de código: “Isso adiciona um novo caso especial?” “A API pode esconder esse detalhe?” “Que complexidade estamos deixando para trás?”
Mantenha registros de decisão curtos para trade-offs não triviais (alguns bullets são suficientes). E reserve um pequeno orçamento de refatoração a cada sprint para que consertos estratégicos não sejam tratados como trabalho extracurricular.
A complexidade não fica aprisionada na engenharia. Ela vaza para cronogramas, confiabilidade e na experiência do cliente.
Quando um sistema é difícil de entender, toda mudança demora mais. O time-to-market escorrega porque cada release exige mais coordenação, mais testes de regressão e mais ciclos de “só por segurança”.
A confiabilidade também sofre. Sistemas complexos criam interações que ninguém consegue prever totalmente, então bugs aparecem como casos de borda: o checkout falha só quando um cupom, um carrinho salvo e uma regra fiscal regional se combinam de uma maneira específica. Esses incidentes são os mais difíceis de reproduzir e os mais lentos de consertar.
O onboarding vira um arrasto oculto. Novos colegas não conseguem formar um modelo mental útil, então evitam áreas arriscadas, copiam padrões que não entendem e, sem querer, adicionam mais complexidade.
Clientes não se importam se um comportamento é causado por um “caso especial” no código. Eles experimentam como inconsistência: configurações que não se aplicam em todo lugar, fluxos que mudam dependendo de como você chegou ali, recursos que funcionam “na maioria das vezes”.
A confiança cai, churn sobe e adoção estagna.
Times de suporte pagam a complexidade com tickets mais longos e mais trocas de mensagens para coletar contexto. Operações paga com mais alertas, mais runbooks e deploys mais cautelosos. Cada exceção vira algo a monitorar, documentar e explicar.
Imagine pedidos por “mais uma regra de notificação”. Adicioná-la parece rápido, mas introduz mais um ramo de comportamento, mais texto na UI, mais casos de teste e mais formas de usuários se confundirem.
Compare isso a simplificar o fluxo de notificação existente: menos tipos de regra, defaults mais claros e comportamento consistente entre web e mobile. Você pode entregar menos botões, mas reduz surpresas—tornando o produto mais fácil de usar, mais fácil de suportar e mais rápido de evoluir.
Trate a complexidade como performance ou segurança: planeje-a, meça-a e proteja-a. Se você só nota a complexidade quando a entrega desacelera, você já está pagando juros.
Junto ao escopo de recursos, defina quanto de nova complexidade um release pode introduzir. O orçamento pode ser simples: “sem conceitos líquidos novos a menos que removamos um” ou “qualquer nova integração deve substituir um caminho antigo”.
Torne trade-offs explícitos no planejamento: se uma feature exige três novos modos de configuração e dois casos excepcionais, isso deve “custar” mais do que uma feature que se encaixa em conceitos existentes.
Você não precisa de números perfeitos—apenas sinais que tendem na direção certa:
Monitore essas por release e vincule-as às decisões: “Adicionamos duas opções públicas; o que removemos ou simplificamos para compensar?”
Prototipos são frequentemente julgados por “conseguimos construir?”. Em vez disso, use-os para responder: “Isso parece simples de usar e difícil de usar errado?”
Peça a alguém não familiar com a feature para tentar uma tarefa realista com o protótipo. Meça tempo para sucesso, perguntas feitas e onde eles cometeram suposições erradas. Esses são pontos quentes de complexidade.
É também onde fluxos modernos de build podem reduzir complexidade acidental—se mantiverem iteração rápida e facilitarem reverter erros. Por exemplo, quando equipes usam uma plataforma de vibe-coding como Koder.ai para esboçar uma ferramenta interna ou um novo fluxo via chat, recursos como modo de planejamento (para esclarecer intenção antes da geração) e instantâneos/reversão (para desfazer mudanças arriscadas rapidamente) podem tornar a experimentação inicial mais segura—sem se comprometer com um monte de abstrações meia-boca. Se o protótipo evoluir, você ainda pode exportar o código-fonte e aplicar a mesma disciplina de “módulos profundos” e design de API descrita acima.
Faça trabalho de “limpeza de complexidade” de forma periódica (trimestral ou a cada release maior) e defina o que “pronto” significa:
O objetivo não é código mais bonito no abstrato—é menos conceitos, menos exceções e mudanças mais seguras.
Aqui estão alguns movimentos que traduzem a ideia de Ousterhout—“a complexidade é o inimigo”—em hábitos semanais de equipe.
Escolha um subsistema que regularmente causa confusão (dor no onboarding, bugs recorrentes, muitas perguntas “como isso funciona?”).
Desdobramentos internos que você pode rodar: uma “revisão de complexidade” no planejamento (/blog/complexity-review) e uma checagem rápida para ver se suas ferramentas estão reduzindo complexidade acidental em vez de adicionar camadas (/pricing).
Que peça de complexidade você removeria primeiro se pudesse deletar um único caso especial esta semana?
Complexidade é a diferença entre o que você espera que aconteça quando altera o sistema e o que realmente acontece.
Você a sente quando pequenas edições parecem arriscadas porque você não consegue prever o raio de impacto (testes, serviços, configurações, clientes ou casos de borda que você pode quebrar).
Procure sinais de que raciocinar está caro:
Complexidade essencial vem do domínio (regulamentações, casos de borda do mundo real, regras de negócio fundamentais). Você não pode removê-la—só modelá-la bem.
Complexidade acidental é autoinfligida (abstrações que vazam, lógica duplicada, modos/bandeiras demais, APIs confusas). Essa é a parte que equipes podem reduzir de forma confiável por meio de design e simplificação.
Um módulo profundo faz muito enquanto expõe uma interface pequena e estável. Ele “absorve” os detalhes espinhosos (retries, formatos, ordenação, invariantes) para que os chamadores não precisem lidar com eles.
Teste prático: se a maioria dos chamadores consegue usar o módulo corretamente sem conhecer regras internas, ele é profundo; se os chamadores precisam memorizar regras e sequências, é raso.
Sintomas comuns:
legacy, skipValidation, force, mode).Prefira APIs que sejam:
Antes de adicionar “só mais uma opção”, pergunte se dá para redesenhar a interface para que a maioria dos chamadores não precise pensar nessa escolha.
Use flags para rollout controlado, depois trate-as como dívida com data de término:
Flags de longa duração multiplicam o número de “sistemas” que os engenheiros precisam raciocinar.
Torne a complexidade explícita no planejamento, não só na revisão de código:
O objetivo é forçar trade-offs a aparecerem antes que a complexidade se institucionalize.
Programação tática otimiza para esta semana: patches rápidos, mudança mínima, “ship it”.
Programação estratégica otimiza para o próximo ano: pequenos redesenhos que removem classes recorrentes de bugs e reduzem trabalho futuro.
Heurística útil: se uma correção exige conhecimento do chamador (“lembre-se de chamar X primeiro” ou “defina essa flag só em prod”), provavelmente você precisa de uma mudança estratégica para esconder essa complexidade dentro do módulo.
A lição duradoura do Tcl é o poder de um pequeno conjunto de primitivas mais composição forte—frequentemente como uma camada embutida de “cola”.
Equivalentes modernos incluem:
O objetivo de design é o mesmo: manter o núcleo simples e estável, e permitir que a mudança ocorra por interfaces limpas.
Módulos rasos frequentemente parecem organizados, mas transferem a complexidade para cada chamador.