8 min

John Ousterhout: design prático, Tcl e o custo da complexidade

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: design prático, Tcl e o custo da complexidade

Por que a mensagem de Ousterhout ainda importa

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 tema central: a complexidade taxa tudo

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.

Três lentes que usaremos neste artigo

Para tornar a ideia concreta (e não apenas motivacional), veremos a mensagem de Ousterhout por três ângulos:

  • Legado do Tcl: o que o Tcl acertou sobre simplicidade, composição e “cola”, e por que essas ideias se espalharam muito além da linguagem.
  • A conexão com Brooks: como “No Silver Bullet” se relaciona com a visão de Ousterhout, onde concordam e o que o desacordo ensina equipes que precisam entregar.
  • Regras práticas de design: especialmente “módulos profundos” e técnicas de design de API que reduzem a carga cognitiva para a próxima pessoa que tiver de mudar o sistema (que geralmente é você).

O que você pode esperar levar daqui

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.

O que “complexidade” realmente significa em equipes do dia a dia

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.

Como a complexidade aparece no trabalho cotidiano

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:

  • Dependências ocultas: um recurso depende silenciosamente de uma coluna de banco, de um job em background ou de uma flag de configuração que não é óbvia a partir do código que você está editando.
  • Casos especiais: “Exceto para clientes enterprise”, “Exceto quando o usuário se inscreveu antes de 2021”, “Exceto se a requisição veio do mobile.” Essas exceções se acumulam até o “caminho normal” ficar obscuro.
  • Responsabilidade pouco clara: ninguém se sente dono de uma área, então correções viram patches cautelosos em vez de melhorias claras. Com o tempo, a rota mais segura vira “adicionar mais um workaround”.

O custo: velocidade, qualidade e confiança

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.

Complexidade essencial vs acidental

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.

Legado do Tcl: as boas ideias que se espalharam

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.

A ideia de “linguagem de cola”

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 que o Tcl acertou (e o que se espalhou)

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.

Limitações e por que o mindshare mudou

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.

Lições de design escondidas na filosofia do Tcl

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.

Um núcleo pequeno que incentiva composição

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.

“Simples de usar” vs “simples de implementar”

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?

Primitivas pequenas podem ser calmantes—ou perigosamente afiadas

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.

Componibilidade vs recursos pontuais (não técnico)

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.

Brooks em uma página: “No Silver Bullet” e sua afirmação

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.

Complexidade essencial vs acidental

Brooks dividiu a complexidade em dois baldes:

  • Complexidade essencial: dificuldade que vem do problema em si—as regras do mundo real, casos de borda e objetivos concorrentes que o software deve representar.
  • Complexidade acidental: dificuldade criada por nossos métodos e ferramentas—linguagens incômodas, pipelines de build atrapalhados, deploys manuais ou arquiteturas que forçam você a pensar em muitos detalhes ao mesmo tempo.

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.

Por que ainda importa

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?

O “Ousterhout vs Brooks” sem calor

Assuma os detalhes de implementação
Mantenha o controle exportando o código-fonte quando estiver pronto para avançar.

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.

A reação de Ousterhout: design compra mais do que se pensa

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.

O aviso de Brooks: parte da complexidade é intrínseca

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.

Onde eles realmente concordam

Eles se sobrepõem mais do que o debate sugere:

  • Parte da complexidade é inevitável porque o mundo é complicado.
  • Muito do sofrimento vem da complexidade acidental—detalhes e exceções que não precisavam existir.
  • O custo real aparece depois: iteração mais lenta, risco maior e áreas “não toque” permanentes.

A pergunta prática para as equipes

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.

Módulos profundos: esconder a complexidade da maneira certa

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

Profundo vs raso: uma analogia do cotidiano

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.

Quando adicionar camadas ajuda (e quando prejudica)

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.

Checklist rápido: identificar módulos rasos

  • A API tem muitos parâmetros, especialmente booleanos como useCache, skipValidation, force, legacy.
  • Chamadores devem seguir uma sequência específica (“chame A antes de B”) para evitar bugs sutis.
  • O módulo vaza conceitos internos (caminhos de arquivo, nomes de tabela, regras de thread) para a interface.
  • A maioria das mudanças exige tocar muitos pontos de chamada porque a abstração não estabiliza o comportamento.
  • Docs soam mais como rótulos de aviso do que promessas (“Não use X quando Y a não ser que Z”).

Módulos profundos não apenas “encapsulam código”. Eles encapsulam decisões.

Design de API que reduz a carga cognitiva

Torne as refatorações mais seguras
Experimente com ousadia e reverta rápido quando uma mudança adicionar complexidade acidental.

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.

O que torna uma API amigável a humanos

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.

Por que “mais opções” eleva o custo de todos

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, convenções e nomenclatura

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

Onde a complexidade se infiltra: correções táticas e casos especiais

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.

Armadilhas comuns que se somam silenciosamente

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 vs estratégica (versão em linguagem simples)

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.

Guardrails simples que realmente ajudam

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.

Por que a complexidade mata produtos, não apenas bases de código

A complexidade não fica aprisionada na engenharia. Ela vaza para cronogramas, confiabilidade e na experiência do cliente.

Custos em nível de produto: velocidade, estabilidade e onboarding

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.

Complexidade aparece como confusão do cliente

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.

O imposto da complexidade sobre suporte e operações

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.

Um exemplo prático: mais uma opção vs fluxos mais simples

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.

Como gerenciar a complexidade como uma restrição de produto de primeira classe

Mantenha a complexidade operacional contida
Execute o app com hospedagem gerenciada para que a configuração não vire um projeto à parte.

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.

Coloque um “orçamento de complexidade” no roadmap

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.

Use métricas leves que equipes consigam manter

Você não precisa de números perfeitos—apenas sinais que tendem na direção certa:

  • Área de superfície do módulo: número de métodos/endpoints públicos, flags ou campos de configuração expostos.
  • Contagem de conceitos: quantas ideias um usuário (ou um engenheiro novo) precisa aprender para ter sucesso.
  • Taxa de falha de mudança: com que frequência deploys ou releases exigem rollback, hotfixes ou trabalho urgente.

Monitore essas por release e vincule-as às decisões: “Adicionamos duas opções públicas; o que removemos ou simplificamos para compensar?”

Prototipe para testar simplicidade, não só viabilidade

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.

Agende limpezas de complexidade com critérios claros de sucesso

Faça trabalho de “limpeza de complexidade” de forma periódica (trimestral ou a cada release maior) e defina o que “pronto” significa:

  • Remover uma opção ou caso especial (não só refatorar).
  • Reduzir passos de onboarding ou configurações obrigatórias.
  • Colapsar duas APIs sobrepostas em uma.
  • Melhorar a taxa de falha de mudança para uma área alvo.

O objetivo não é código mais bonito no abstrato—é menos conceitos, menos exceções e mudanças mais seguras.

Conclusões práticas para equipes neste trimestre

Aqui estão alguns movimentos que traduzem a ideia de Ousterhout—“a complexidade é o inimigo”—em hábitos semanais de equipe.

5–7 conclusões diretas

  • Trate a complexidade como um centro de custos: se não compra valor ao usuário, precisa de aprovação de orçamento.
  • Prefira menos módulos, mais profundos em vez de muitas camadas finas que vazam detalhes.
  • Mire em interfaces que se explicam: bons nomes, superfície pequena, invariantes claras.
  • Não “simplesmente adicione uma opção.” Opções multiplicam interações; casos especiais se acumulam com o tempo.
  • Se uma correção exige conhecimento extra do chamador, provavelmente você moveu a complexidade para fora.
  • Faça da exclusão uma métrica de sucesso: remover código e casos é muitas vezes o trabalho de design de maior alavancagem.

Um plano de ação curto (1–2 semanas)

Escolha um subsistema que regularmente causa confusão (dor no onboarding, bugs recorrentes, muitas perguntas “como isso funciona?”).

  1. Mapeie a interface: liste funções/endpoints públicos/flags de configuração e o que os chamadores precisam saber.
  2. Simplifique o contrato: consolide parâmetros, remova flags de “modo” e escreva 2–3 invariantes que o módulo garante.
  3. Delete casos especiais: remova ramificações adicionadas para um cliente, um ambiente ou um bug histórico—e então substitua por uma regra geral.
  4. Adicione um portão leve: novas flags e exceções exigem uma nota de design curta e um revisor que pergunte, “Podemos evitar este caso especial?”

Leituras adicionais e desdobramentos

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

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?

Perguntas frequentes

O que “complexidade” significa no trabalho cotidiano de software?

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

Como uma equipe pode detectar complexidade cedo, antes que vire crise?

Procure sinais de que raciocinar está caro:

  • O comportamento depende de dependências ocultas (uma coluna, job, configuração ou cache que você não sabia que importava).
  • O “caminho normal” está confuso por causa de exceções empilhadas (“exceto enterprise”, “exceto contas antigas”).
  • Mudanças exigem coordenação entre muitas pessoas/serviços por segurança.
  • Documentação e comentários soam como rótulos de aviso (“não chame X quando Y a menos que Z”).
Qual é a diferença entre complexidade essencial e acidental?

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.

O que é um “módulo profundo” e por que ele importa?

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.

Como reconhecer um módulo raso ou uma abstração que vaza?

Sintomas comuns:

  • Muitos parâmetros e booleanos (legacy, skipValidation, force, mode).
  • Ordem de chamada requerida (“chame A antes de B”) que não é imposta pela API.
  • Conceitos internos vazam para a interface (nomes de tabela, caminhos de arquivo, chaves de cache).
  • Pequenas mudanças se propagam para muitos pontos de chamada.

Módulos rasos frequentemente parecem organizados, mas transferem a complexidade para cada chamador.

Quais são regras práticas para desenhar APIs que reduzam a carga cognitiva?

Prefira APIs que sejam:

  • Pequenas e consistentes: poucos conceitos que se combinam.
  • Difíceis de usar errado: validação nas fronteiras, invariantes claras, padrões seguros.
  • Com baixa combinatória: evite explosão de opções em que flags interagem de forma imprevisível.

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.

Como as equipes devem gerenciar feature flags para que não originem complexidade permanente?

Use flags para rollout controlado, depois trate-as como dívida com data de término:

  • Adicione um plano de remoção quando a flag for criada (dono + prazo).
  • Apague flags obsoletas regularmente; consolide as sobrepostas.
  • Evite flags que alterem semântica em vários pontos—prefira um único limite onde a decisão é tomada.

Flags de longa duração multiplicam o número de “sistemas” que os engenheiros precisam raciocinar.

O que significa colocar um “orçamento de complexidade” no roadmap?

Torne a complexidade explícita no planejamento, não só na revisão de código:

  • Estabeleça uma regra como “sem novos conceitos líquidos a menos que removamos um”.
  • Cobre escopo extra por recursos que introduzem novos modos, configs ou exceções.
  • Monitore sinais simples por release (endpoints/opções públicas adicionadas, campos de configuração adicionados, taxa de falha de mudança).

O objetivo é forçar trade-offs a aparecerem antes que a complexidade se institucionalize.

Qual é a diferença entre programação tática e estratégica na prática?

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.

O que equipes modernas podem aprender com a filosofia “glue language” do Tcl?

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:

  • Sistemas de plugin/extensão com primitivas hospedeiras estáveis.
  • Camadas de scripting ou políticas para automação (ops, QA, ferramentas internas).
  • Linguagens de configuração que mantêm o núcleo estável enquanto permitem composição flexível.

O objetivo de design é o mesmo: manter o núcleo simples e estável, e permitir que a mudança ocorra por interfaces limpas.

Related posts