Agents | Versão do Blog | Original
Este é um documento vivo, última atualização da tradução: Janeiro de 2026 e última atualização do documento original: Outubro de 2025. Contribuições são bem vindas!
Extistem muitas palavras na moda e "melhores práticas" por aí, mas maior parte delas tem falhado. Elas falharam porque foram imaginadas, ao invés de se basearem na realidade. Essas ideias foram baseadas na estética e julgamentos subjetivos. Precisamos de algo mais fundamental, algo que não possa estar errado.
Algumas vezes sentimos uma confusão ocorrendo no código. Confusão que custa tempo e dinheiro. Confusão esta, causada pela alta carga cognitiva. Este não é apenas algum conceito abstrato chique, mas uma limitação fundamental humana. Não é imaginado, é algo que está lá e podemos sentir.
Já que gastamos muito mais tempo lendo e compreendendo código que escrevendo, poderíamos constantemente nos questionar se estamos embarcando carga cognitiva excessiva em nosso código.
Carga cognitiva é o quanto um desenvolvedor precisa pensar para completar uma tarefa.
Quando lê-se o código, você coloca coisas como valores de variáveis, controles de fluxo e sequências de chamadas em sua cabeça. A pessoa média consegue guardar aproximadamente quatro desses blocos em sua memória de trabalho. Uma vez que a carga cognitiva atinge tal limite, se torna muito mais difícil de compreender as coisas.
Digamos que fomos pedidos para fazer alguns consertos em um projeto completamente não-familiar. Disseram-nos que um desenvolvedor realmente inteligente tem contribuído para o mesmo. Muitas arquiteturas legais, bibliotecas fantásticas e tecnologias novas e da moda foram usadas. Em outras palavras, O autor criou uma alta carga cognitiva deixada para nós.
Deveríamos reduzir a carga cognitiva em nossos projetos o máximo possível.
Usaremos "carga cognitiva" de maneira informal; por vezes alinhará com o conceito científico específico de Carga Cognitiva, mas não sabemos o suficiente quando alinhará ou não.
Intrínseca — causada pela dificuldade inerente da tarefa. Não pode ser reduzido, já que se está na cerne do desenvolvimento do software.
Extrínseca — criada pela forma que a informação é apresentada. Causada por fatores não diretamente relevantes à tarefa, como as peculiaridades do autor esperto. Pode ser largamente reduzida. Focaremos nesse tipo de carga cognitiva.
Pularemos direto aos exemplos práticos e concretos de carga cognitiva extrínseca.
Referiremos ao nível de carga coginitiva como o seguinte:
🧠: memória de trabalho fresca, nenhuma carga coginitiva.🧠++: dois fatos em nossa memória de trabalho, carga cognitiva aumentada.🤯: sobrecarga cognitiva, mais de 4 fatos
Nosso cérebro é muito mais complexo e não explorado, mas podemos seguir neste modelo simplístico.
if val > someConstant // 🧠+
&& (condition2 || condition3) // 🧠+++, condição prévia deve ser verdadeira, uma de c2 ou c3 tem de ser verdadeira
&& (condition4 && !condition5) { // 🤯, estamos confusos neste momento
...
}Introduza variáveis intermediárias com nomes significativos:
isValid = val > someConstant
isAllowed = condition2 || condition3
isSecure = condition4 && !condition5
// 🧠, não precisamos lembrar as condições, elas são variáveis descritivas
if isValid && isAllowed && isSecure {
...
}if isValid { // 🧠+, ok, código aninhado se aplica a entradas válidas, apenas
if isSecure { // 🧠++, fazemos a tarefa apenas para entradas válidas e seguras
stuff // 🧠+++
}
}Compare agora com early returns:
if !isValid
return
if !isSecure
return
// 🧠, não precisamos ligar por conta dos retornos breves. Se estamos aqui, então tudo certo
stuff // 🧠+Podemos apenas focar no caminho feliz, liberando nossa memória de trabalho de todos esses tipos de pré-condições.
Fomos pedidos para mudar algumas coisas para nossos usuários administradores: 🧠
AdminController extends UserController extends GuestController extends BaseController
Ahh, parte da functionalidade está em BaseControler, vamos dar uma olhada: 🧠+
O mecanismo básico foi introduzido em GuestController: 🧠++.
Algumas coisas foram parcialmente alteradas em UserController: 🧠+++.
Finalmente, estamos aqui, AdminController, vamos programar nossa tarefa! 🧠++++.
Ei, espera! Existe um SuperuserController que extende AdminController. Ao modificar AdminController, podemos quebrar partes da classe herdeira, vamos mergulhar em SuperuserController primeiro: 🤯
Prefira composição à herança. Não vamos entrar em muitos detalhes — existem uma variedade de materiais por aí.
Método, classes e módulos são intercambiáveis neste contexto.
Mantras como "métodos deveriam ser menores que 15 linhas de código" ou "classes deveriam ser pequenas" se tornaram em algo errado.
Módulo profundo — interface simples, funcionalidade complexa Módulo raso — interface relativamente complexa comparada à pequena funcionalidade que isso provê.
Ter muitos módulos rasos pode tornar o projeto difícil de compreender. Não apenas temos de manter em mente a responsabilidade de cada módulo, como também suas interações. Para compreender o propósito de um módulo raso vamos precisar olhar para a funcionalidade de todos os módulos relacionados. Pular entre cada componente raso é mentalmente exaustivo pensamento linear é mais natural para nós, humanos.
Ocultar informação é fundamental. Não precisamos ocultar tanto a complexidade em módulos rasos.
Eu tenho dois projetos pet. Ambos com algo entre 5 mil linhas de código. O primeiro tem 80 classes rasas, enquanto o segundo tem apenas 7 classes profundas. Eu não tenho mantido nenhum dos dois projetos por um ano e meio.
Ao retornar, eu percebi o quão extremamente difícil é para desembaraçar todas as interações entre essas 80 classes do primeiro projeto. Eu teria de reconstruir a quantidade enorme de carga coginitiva antes que pudesse começar a programar. Por outro lado, eu fui capaz de compreender o segundo projeto rapidamente, já que tinham apenas algumas classes profundas com interfaces simples.
Os melhores componentes são aqueles que provém funcionalidades poderosas enquanto mantém interfaces simples.
John Ousterhout, Um Filósofo de Deisgn de Software
A interface do UNIX I/O é bastante simples. Ele tem apenas cinco básicas chamadas.
open(caminho, bandeiras, permissões)
read(da, buffer, contagem)
write(da, buffer, contagem)
lseek(da, desvio, posiçãoDeReferencia)
close(da)Uma implementação moderna dessa interface tem centeras de milhares de linhas de código. Muitas das complexidades estão ocultas por debaixo do capô. Ainda assim, é fácil de usar devido a sua simples interface.
Esse exemplo de módulo profundo é tirado do livro A Philosophy of Software Design por John Ousterhout. Não apenas este livro cobre bastante a essência da complexidade no desenvolvimento de Software, mas também tem a melhor interpretação do artigo influencial de Parnas On the Criteria To Be Used In Decomposing Systems into Modules. Ambos são essenciais de ler. Outras leituras relacionadas: A Philosophy of Software Design vs Clean Code, It's probably time to stop recommending Clean Code, Small Functions considered Harmful.
Coisas importantes deviam ser grandes, exemplos
Se você permitir seu "crux" de funções serem maiores, sujas, será mais fácil de escolhê-las dentro de um mar de funções, elas serão obviamente importantes: apenas olhe para elas, já que grandes!
Esta imagem foi tiradas de Codin' Dirty, artigo por Carson Gross. Você encontrará exemplos do mundo real de funções profundas aí.
P.S. Caso pense que estamos enraizando objetos divinos inchados com muitas responsabilidades, você compreendeu errado.
Frequentemente, acabamos por criar muitos módulos rasos, seguindo vagos princípios como "um módulo deveria ser responsável por uma coisa, e apenas uma única coisa". O que significa isso? Instanciar um objeto é uma única coisa, certo? Então MetricsProviderFactoryFactory parece ser correto. Os nomes e interfaces destas classes tendem a ser mentalmente desgastantes. Mais que sua implementação completa. Que tipo de abstração é essa? Algo está errado.
Fazemos mudanças em nossos sistemas para satisfazer nossos usuários e Stakeholders. Somos responsáveis por eles.
Um módulo deveria ser responsável por um, e apenas um, usuário ou Stakeholder.
Isso é o que o Princípio da Responsabilidade Única (Single Responsibility Principle) é sobre. Em termos simples, se introduzirmos um bug em um lugar e, em seguida, dois profissionais de negócios diferentes vierem reclamar, teremos violado o princípio. Isso não tem nada a ver com o número de coisas que fazemos em nosso módulo.
Mas ainda assim, esta regras pode ser mais danosa que boa. Esse princípio pode ser compreendido de múltiplas formas como há de pessoas na terra. Uma forma melhor seria a de olhar para a quantidade de carga cognitiva que ele pode criar. Esta é a demanda mental para lembrar que mudar em um lugar pode ativar uma cadeia de reações em diferentes fluxos de negócios.
Esse princípio de módulo raso-profundo é agnóstico à escala, e podemos aplicá-lo à arquitetura de microsserviços. Uma quantidade excessiva de microsserviços rasos não traz nenhum benefício — a indústria tem caminhado em direção a "macrosserviços", exemplo, serviços que não são tão rasos (=profundos). Um dos piores fenômenos e mais dificéis de consertar é o chamado monolito distribuído, o qual é frequentemente resultado de uma separação rasa excessivamente granular.
Certa vez eu consultei uma Startup onde um time de cinco desenvolvedores introduziram 17 (!) microsserviços. Eles estavam 10 meses atrasados e nem um pouco próximo do lançamento público. Cada novo requisito levava a mudanças em mais de 4 microsserviços. Isso levou uma quantidade enorme de tempo para reproduzir e depurar um problema como um sistema distribuído. Ambos, o estresse com prazos e a carga coginitiva estavam inaceitavelmente altos. 🤯
Esta é a forma correta de lidar com a incerteza de um novo sistema? É extremamente difícil eleger os limites lógicos no início. A chave é tomar decições o mais tarde que você responsavelmente possa esperar, pois terá a informação nas mãos. Ao introduzir uma camada de network logo de cara, fazemos nossa decisão de design difícil de reverter logo do início. A única justificativa do time foi: "Empresas FAANG provaram que a arquitetura de microsserviços é efetiva". Olá, você precisa parar de sonhar alto.
O debate Tanenbaum-Torvalds argumentou que o design monolítico do Linux era falho e obsoleto, e que a arquitetura de microkernel deveria ser usada ao invés. De fato, o design de microkernel parecia ser superior do ponto de vista "teorético e estético". No lado prático das coisas — três décadas depois, o GNU Hurd baseado em microkernel continua em desenvolvimento, e o Linux monolítico está em todo canto. Essa página é entregue por um Linux, seu bule inteligente utiliza Linux. Linux monolito.
Um monolito bem-feito com módulos verdadeiramente isolados é frequentemente muito mais flexível que um monte de microsserviços. Ele requer muito menos esfoço cognitivo para manter. Apenas quando separar deployments se torna crucial, como escalar o desenvolvimento de time, que devemos considerar adicionar uma camada de network entre os módulos, futuro microsserviços.
Ficamos ansiosos por novos recursos lançados em nossas linguagem favoritas. Gastamos certo tempo para aprender esses recursos, e construímos código em cima disso.
Se existem vários recursos, podemos gastar meia hora brincando com algumas linhas de código para usar um ou outro recurso. E isso é meio que uma perda de tempo. Mas o que é pior, quando retornar depois, você poderá ter de recriar todo o processo de pensamento!
Você não apenas tem de compreender esse programa complicado, como tem de compreender porque um programador decidiu essa forma de resolver o problem com os recursos disponíveis. 🤯
Essas afirmações foram feitas por ninguém menos que Rob Pike.
Reduza a carga cognitiva pelo número de escolhas.
Recursos de linguagem são OK, contanto que sejam ortoginais entre si.
Pensamentos de um engenheiro com 20 anos de experiência em C++ ⭐️
Eu estava procurando em meu leitor de RSS outro dia e notei que eu tenho algo entre trezentos artigos sobre linguagens desde o último verão, e eu me sinto bem!
Eu tenho usado C++ por 20 anos, o que são quase dois terços de minha vida. Maior parte de minha experiência deriva dos cantos mais obscuros da linguagem (como undefined behavior de todos os tipos). Isso não é apenas uma experiência reusável, commo é o tipo de coisa assustadora para se jogar fora.
Tipo, você pode imaginar, o token
|| tem significado distinto em requires ((!P<T> || !Q<T>)) e em requires (!(P<T> || Q<T>)). A primeira é a disjunção de restrição, a segunda é o bom e velho operador lógico OR, e elas se comportam de maneira diferente.Você não consegue alocar espaço para um tipo trivial e apenas chamar
memcpy para um conjunto de bytes sem esforço extra — isso não iniciará o ciclo-de-vida do objeto. Este foi o caso antes do C++20. E foi consertado no C++20, mas a carga cognitiva da linguagem tem apenas aumentado.A carga cognitiva está constantemente crescendo, mesmo quando as coisas são consertadas. Eu deveria saber o que foi consertado, quando isso foi consertado e como era antes. Eu sou um profissional afinal de contas. Certo, C++ é bom para suporte legado, o que significa que você irá enfrentar o legado. Por exemplo, último mês um colega meu me perguntou sobre o comportamento em C++03.
🤯Existiram 20 formas de inicialização. A sintaxe uniforme de inicialização foi adicionado. Agora temos 21 formas de inicialização. De qualquer forma, alguém lembra as regras para selecionar construtores de uma lista inicializadora? Algo sobre conversão implícita com o mínimo de perda de informação mas se o valor é conhecido estaticamente, então...
🤯Esse aumento na carga cognitiva não é causado pela tarefa em mãos. E não é uma complexidade intrínseca do domínio. É apenas algo que está lá devido a questões históricas (Carga cognitiva extrínseca).
Eu tive de criar algumas regras. Assim, se uma linha de código não for óbvia e eu tivesse de lembrar do Standard, é melhor não escrever dessa forma. O Standard é longo, de 1500 páginas, a propósito.
De nenhuma forma estou tentando julgar C++. Amo a linguagem. Mas estou apenas cansado por agora.
Obrigado ao 0xd34df00d por escrever.
No backend, retornamos:
401para tokens JWT expirados403para acesso insuficiente418para usuários banidos
Os engenheiros no frontend usam a API para implementar a funcionalidade de login. Eles precisariam de temporariamente criar a seguinte carga cognitiva em suas cabeças:
401para tokens JWT expirados:🧠+, ok vamos temporariamente lembrar disso403para acesso insuficiente:🧠++418para usuários banidos:🧠+++
Desenvolvedores frontend deveriam (com sorte) introduzir algum tipo de dicionário (status numérico → significado) em seu lado, então as gerações subsequentes de contribuidores não precisaram ter de recirar esse mapa em sua mente.
Então os engenheiros QA entram na jogada:
"Ei, eu tenho um status 403, isso seria o token expirado ou acesso insuficiente?"
Engenheiros QA não conseguem pular direto aos testes, pois eles primeiro têm de recriar a carga cognitiva que os engenheiros backend uma vez criaram.
Por que guardar esse mapa customizado em nossa memória de trabalho? É melhor abstrair os nossos detalhes de negócio do protocolo de transferência HTTP e retornar códigos auto-descritíveis no corpo da resposta:
{
"código": "jwt-expirado"
}Carga cognitiva do lado do front-end: 🧠 (fresco, nenhum fato é guardado em mente).
Carga cognitiva do lado do QA: 🧠.
As mesmas regras se aplicam a todos os tipos de status numéricos (em bancos de dados ou qualquer outra coisa) — prefira strings auto-descritíveis. Não estamos em uma era de computadores com 640K de memória para otimizar.
Pessoas gastam tempo argumentando entre
401e403, tomando decisões baseadas em seus próprios modelos mentais. Novos desenvolvedores estão chegando, e eles precisam de recriar esse processo de pensamento. Você pode ter documentado os Porquês (ADRs) para o seu código, ajudando novatos a compreender as decisões feitas. Mas no final apenas não fazem sentido. Nós podemos separar erros entre ambos, relacionados-ao-usuário ou relacionados-ao-servidor, mas além disso, as coisas são muito obscuras.
P.S. Muitas vezes, é mentalmente cansativo distinguir entre “autenticação” e “autorização”. Podemos usar termos mais simples, como “login” e “permissões”, para reduzir a carga cognitiva.
Não se repita (Don't Repeat Yourself, DRY) — é um dos primeiros princípios que você é ensinado como um engenheiro de software. Está tão profundamente embarcado em nós que não podemos aguentar o fato de algumas linhas extras de código. A pesar de ser, em geral, uma regra boa e fundamental, quando sobre-usada, leva a uma carga cognitiva que não poodemos suportar.
Hoje em dia, todo mundo contrói software baseado em componentes logicamente separados. Frequentemente, são distribuídos entre múltiplas bases de código representando serviços separados. Quando você tenta eliminar qualquer repetição, você pode acabar por criar um acoplamento estreito entre componentes não relacionados. Como resultado, mudanças em uma parte pode levar a consequências não intencionais em outras áreas aparentemente não relacionadas. Isso também pode atrapalhar a capacidade de trocar ou modificar componentes individuais sem impactar em sistemas completos. 🤯
De fato, o mesmo problema surge mesmo dentro de um único módulo. Você pode extrair funcionalidades comuns muito cedo, baseado em similaridades pecebidas que podem não realmente existir no longo prazo. Isso pode resultar em abstrações desnecessárias que são difíceis de estender ou modificar.
Rob Pike disse, certa vez:
Uma pequena cópia é melhor que uma pequena dependência.
Somos tentados a não re-inventar a roda tão fortemente que estamos prontos para importar biliotecas grandes e pesadas para usar pequenas funções que poderíamos escrever nós mesmos.
Todas as suas dependências são seu código. Indo através de mais de 10 níveis de Stack Trace de alguma biblioteca importada e interpretar o que está de errado (porque as coisas dão errado) e é doloroso.
Existem várias mágicas em frameworks. Ao depender muito de frameworks, nós forçamos todos os desenvolvedores a aprender essa "mágica" primeiro. Isso pode levar meses. A pesar disso, frameworks nos permitem criar Produtos Mínimos Viáveis (Minimal Viable Products, MVP) em poucos dias, no longo prazo eles tendem a adicionar complexidade desnecessária e carga cognitiva.
Pior ainda, a algum ponto os frameworks podem se tornar uma retrição significativa quando encarar um novo requerimento que não se encaixa na arquitetura. Até que as pessoas acabem por bifurcar um framework e manter suas próprias versões customizadas. Imagine a quantidade de carga cognitiva que um novato teria de construir (por exemplo, aprender esse framework customizado) para que possa entregar algum valor. 🤯
De nenhuma forma eu advogo em pró de inventar tudo do zero!
Podemos programar de uma forma agnóstica à frameworks. A regra de negócios não deveriam residir dentro de um framework; ao invés disso, deveriamos usar os componentes do framework. Use framework no estilo de bibliotecas. Dessa forma, permitimos que novos contribuidores adicionem valor do dia um, sem necessidade de mergulhar em detritos de complexidade relacionadas ao framework primeiro.
Há uma certa emoção de engenharia em tudo isso.
Eu era um apaixonado advogado de arquitetura Hexagonal/Onion por anos. Usei e encorajei outros times a usar também. A complexidade de nossos projetos aumentaram, a quantidade de arquivos sozinhos dobraram. Parece como se eu estivesse escrevendo um monte de código-cola. A cada mudança de requerimento, temos de fazer mudanças entre múltiplas camadas de abstração, o que se torna tedioso. 🤯
Abstrações são supostamente para ocultar complexidade, não adicionar indireção. Pulando de chamada em chamada para ler e compreender o que há de errado e qual é o requerimento vital para rapidamente resolver o problema. Com essa arquitetura em camada, desacoplar requer um fator extra exponencial, frequentemente desarticulado, para chegar ao ponto onde a falha ocorre. Cada stack trace ocupa espaço em nossa limitada memória de trabalho. 🤯
Essa arquitetura foi algo que fez sentido intuitivo de primeira, mas cada vez que tentamos aplicá-la a projetos, isso fez mais mal que bem. Gastamos anos em atividade mental desnecessária e escrevemos código-cola inútil sem valor de negócios claro. Ao contrário, tornamos as coisas piores para os negócios ao forçar novatos a aprender nossas escolhas (modelos mentais) primeiro. O tempo de mercado tem piorado. E no final, desistimos em favor do bom e velho princípio de inversão de dependência. Nenhum termo de porta ou adaptador para aprender, nenhuma camada horizontal de abstração, nenhuma carga coginitiva extrínseca.
Se você pensa que tal camadas irão te permitir rapidamente trocar o banco de dados ou quaisquer outras dependências, você está enganado. Mudar o armazenamento causa vários problemas, e acredite em nós, ter algumas abstrações para acessar a camada de dado é a última de suas preocupações. Ao menos, abstrações podem salvar algo entre 10% do nosso tempo de migração (se houver), a dor real está em incompatibilidades do modelo de dado, protocolo de comunicações, desafios de sistemas distrbuídos e interfaces implícitas.
Com um número suficiente de usuários de uma API, não importa qual a sua promessa no contrato: todos os comportamentos de seu sistema será dependente de alguém.
Fazemos uma migração de armazenamento, e levamos algo entre 10 meses. O sistema legado era single-thread, então os eventos expostos eram sequenciais. Todos os nossos sistemas dependem naquele comportamento observado. Esse comportamento não era parte de nosso contrato de API, e não refletia em nosso código. Um novo armazenamento distribuído não nos garantiu isso — os eventos chegam fora-de-ordem. Gastamos apenas algumas horas programando o novo adaptador do armazenamento, agradecimentos a uma abstração. Gastamos os próximos 10 meses lidando com eventos fora-de-ordem e outros desafios. Agora é engraçado dizer que abstrações nos ajudam a trocar componentes rapidamente.
Então, porque pagar o preço de carga cognitiva como uma arquitetura em camadas, se podemos não pagar no futuro? Adicionalmente, na maioria dos casos, o futuro de trocar algum componente principal nunca acontece.
Essas arquiteturas não são fundamentais, elas são apenas subjetivas, com consequências enviesadas de princípios mais fundamentais. Por que confiar nessas interpretações subjetivas? Siga regras fundamentais ao invés disso: princípio de invesão de dependência, única fonte de verdade, carga cognitiva e ocultação de informação. Nossa lógica de negócios não deveriam depender de módulos de baixo-nível, como banco de dados, interfaces de usuário ou frameworks. Deveriamos ser capazes de escrever testes para nossa lógica principal sem ter de nos perocupar com infraestrutura, e é isso. Dicussão
Não adicione camadas de abstração por conta da arquitetura. Adicione-as quando precisar de uma extensão a algum ponto como é justificável por razões práticas.
Camadas de abstrações não são livres de mudanças, elas são guardadas em nossa limitada memória de trabalho.
Domain-driven design (DDD) tem alguns pontos positivos, apesar disso, é frequentemente mal-interpretado. Pessoas dizem "Nós escrevemos código DDD", o qual é um pouco estranho, já que DDD é mais sobre o espaço do problema que o espaço de solução.
Linguagem ubíqua, domínio, contexto adjunto, agregado, event storming são todos espaços de problemas. Eles são para nos ajudar a aprender os insights sobre o domínio e extrarir seus limites. DDD permite desenvolvedores, especialistas de domínio e pessoas de negócios se comunicarem de forma efetiva ao utilizar uma linguagem unificada e singular. Ao invés de focar nesses aspectos do DDD sobre o espaço do problrma, tendemos a dar ênfase em estruturas de arquivos em particular, serviços, repositórios e outras técnicas de solução de espaço.
Chances são que a forma como interpretamos DDD é única e subjetiva. E se construirmos código em cima dessa compreensão, por exemplo, se criamos uma carga cognitiva extrínseca, desenvolvedores futuros estão condenados. 🤯
Team Topologies provê um framework muito melhor e fácil de compreender que nos permite dividir a carga cognitiva entre times. Engenheiros tendem a desenvolver modelos mentais similares depois de aprender sobre Team Topologies. DDD, por outro lado, parece estar criando 10 modelos mentais diferentes para 10 leitores distintos. Ao invés de um chão comum, isso se torna um campo de batalha para debates desnecessários.
O problema está no fato que familiaridade não é o mesmo que simplicidade. Eles parecem a mesma coisa — mesma facilidade de se mover no espaço sem muito esforço mental — mas por razões muito diferentes. Cada truque "esperto" (leia-se: “autoindulgente”) e não idiomático que você usa acarreta uma penalidade de aprendizagem para todos os outros. Uma vez que eles aprendem, é mais fácil trabalhar com o código. Então é difícil reconhecer como é difícil simplificar um código já familiarizado. É por isso que eu peço aos "novatos" que critiquem o código antes que fiquem muito institucionalizados!
É como se o(s) autor(es) anterior(res) tivesse(m) criado essa grande bagunça um incremento por vez, e não tudo de uma só vez. Então você é a primeira pessoa que tentou encontrar sentido nisso tudo de uma só vez.
Em minha aula, eu descrevo um extenso procedimento SQL armazenado onde estávamos analisando um dia, com centenas de linhas de condicionais em uma grande cláusula WHERE. Alguém me perguntou como eu pude deixar isso ficar assim tão ruim. E disse: "Quando há apenas 2 ou 3 condicionais, adicionar mais uma não faz qualquer diferença. Mas com o tempo, você terá 20 ou 30 condicionais, e adicionar outras não fará nenhuma diferença!"
Não existe "força de simplificação" atuando no código-base outras além de escolhas deliberadas que você faz. Simplificar demanda esforço. E pessoas, muitas vezes, estão com pressa.
Obrigado ao Dan North por seu comentário.
Se tiver internalizado os modelos mentais do projeto em sua memória de longo-termo, não experienciará a carga cognitiva.
Quanto mais modelos mentais tiver de aprender, mais tempo levará para um novo desenvolvedor entregar valor.
Uma vez que você embarca novas pessoas em seu projeto, tente medir a quantidade de confusão que eles têm (Pair Programming pode ajudar). Caso eles estejam confusos por mais de, mais ou menos, 40 minutos de uma só vez, você tem coisas a melhorar em seu código.
Se você mantiver a carga cognitiva baixa, pessoas podem contribuir para seu código-base dentro de algumas poucas horas ao entrar na empresa.
- Nossa arquitetura é uma aplicação CRUD padrão, Um monolito Python em cima do Postgres.
- Como o Instagram escalou para 14 milhões de usuários com apenas 3 engenheiros.
- As empresas que nos deixaram tipo "Nossa! Esses caras são inteligentes para caramba" acabaram, na sua maioria, fracassando.
- Uma função que liga todo o sistema. Se você quiser saber como o sistema funciona, leia isto.
Essas arquiteturas são bastante chatas e fáceis de compreender. Qualquer um pode compreendê-las sem muito esforço mental.
Envolva desenvolvedores júniors em suas reviews de arquitetura, eles podem te ajudar a identificar áreas mentalmente exaustivas.
Sistemas de software são talvez os mais intricados e complexos (em termos de número de tipos distintos de peças) entre as coisas que a humanidade cria.
Fred Brooks, The Mythical Man-Month
Manter software é difícil, as coisas quebram e precisamos de todo o esforço mental que pudermos economizar. Quanto menos componentes houver no sistema, menos problemas haverá. A depuração também será menos desgastante mentalmente.
Depurar é duas vezes mais difícil do que escrever o código, em primeiro lugar. Portanto, se você escrever o código da forma mais esperta possível, você, por definição, não será inteligente o suficiente para depurá-lo.
Brian Kernighan
Em geral, a mentalidade de "Uau, essa arquitetura parece ótima!" é enganosa. Essa é uma sensação subjetiva de um determinado momento e não diz nada sobre a realidade. Uma abordagem muito melhor é observar as consequências a longo prazo:
- É fácil reproduzir e depurar um problema? Ou você precisa pular entre call stacks ou componentes distribuídos, tentando entender tudo na sua cabeça?
- Podemos fazer alterações rapidamente ou há muitas incógnitas e as pessoas têm medo de mexer nas coisas?
- Os novos funcionários podem adicionar recursos rapidamente? Há alguns modelos mentais únicos a serem aprendidos?
O que são esses modelos mentais únicos? É um conjunto de regras, geralmente uma mistura de DDD/CQRS/Arquitetura Limpa/Arquitetura Orientada a Eventos. Essa é a interpretação do autor sobre as coisas que mais o entusiasmam. Seus próprios modelos mentais subjetivos.
Carga cognitiva estranha que os outros têm de internalizar.
Essas questões são muito mais difíceis de rastrear, e as pessoas geralmente não gostam de respondê-las diretamente. Veja alguns dos sistemas de software mais complexos do mundo, aqueles que resistiram ao teste do tempo — Linux, Kubernetes, Chrome e Redis (veja os comentários abaixo). Você não encontrará nada sofisticado neles, são em sua maioria enfadonhos, e isso é bom.
Imagine por um momento que o que inferimos no segundo capítulo não seja realmente verdade. Se for este o caso, então a conclusão que acabamos de negar, juntamente com as conclusões do capítulo anterior que aceitamos como válidas, também podem não estar corretas.
Você percebeu? Não só você precisa pular de um lado para outro no artigo para entender o significado (módulos superficiais!), como o parágrafo em geral é difícil de entender. Acabamos de criar uma carga cognitiva desnecessária na sua cabeça. Não faça isso com seus colegas.
Deveríamos reduzir qualquer carga cognitiva acima. É algo além de instrínseco para o trabalho que fazemos.
LinkedIn, X, GitHub, artemzr(аt)g-yоu-knоw-com
Rob Pike (Unix, Golang)
Nice article.
Andrej Karpathy (ChatGPT, Tesla)
Nice post on software engineering. Probably the most true, least practiced viewpoint.
Elon Musk (Rockets)
True.
Addy Osmani (Chrome, the most complex software system in the world)
I've seen countless projects where smart developers created impressive architectures using the latest design patterns and microservices. But when new team members tried to make changes, they spent weeks just trying to understand how everything fits together. The cognitive load was so high that productivity plummeted and bugs multiplied.
The irony? Many of these complexity-inducing patterns were implemented in the name of "clean code."
What really matters is reducing unnecessary cognitive burden. Sometimes this means fewer, deeper modules instead of many shallow ones. Sometimes it means keeping related logic together instead of splitting it into tiny functions.
And sometimes it means choosing boring, straightforward solutions over clever ones. The best code isn't the most elegant or sophisticated - it's the code that future developers (including yourself) can understand quickly.
Your article really resonates with the challenges we face in browser development. You're absolutely right about modern browsers being among the most complex software systems. Managing that complexity in Chromium is a constant challenge that aligns perfectly with many of the points you made about cognitive load.
One way we try to handle this in Chromium is through careful component isolation and well-defined interfaces between subsystems (like rendering, networking, JavaScript execution, etc.). Similar to your deep modules example with Unix I/O - we aim for powerful functionality behind relatively simple interfaces. For instance, our rendering pipeline handles incredible complexity (layout, compositing, GPU acceleration) but developers can interact with it through clear abstraction layers.
Your points about avoiding unnecessary abstractions really hit home too. In browser development, we constantly balance between making the codebase approachable for new contributors while handling the inherent complexity of web standards and compatibility.
Sometimes the simplest solution is the best one, even in a complex system.
antirez (Redis)
Totally agree about it :) Also, what I believe is missing from mentioned "A Philosophy of Software Design" is the concept of "design sacrifice". That is, sometimes you sacrifice something and get back simplicity, or performances, or both. I apply this idea continuously, but often is not understood.
A good example is the fact that I always refused to have hash items expires. This is a design sacrifice because if you have certain attributes only in the top-level items (the keys themselves), the design is simpler, values will just be objects. When Redis got hash expires, it was a nice feature but required (indeed) many changes to many parts, raising the complexity.
Another example is what I'm doing right now, Vector Sets, the new Redis data type. I decided that Redis would not be the source of truth about vectors, but that it can just take an approximate version of them, so I was able to do on-insert normalization, quantization without trying to retain the large floats vector on disk, and so forth. May vector DBs don't sacrifice the fact of remembering what the user put inside (the full precision vector).
These are just two random examples, but I apply this idea everywhere. Now the thing is: of course one must sacrifice the right things. Often, there are 5% features that account for a very large amount of complexity: that is a good thing to kill :D
A developer from the internet
You would not hire me... I sell myself on my track record of released enterprise projects.
I worked with a guy that could speak design patterns. I could never speak that way, though I was one of the few that could well understand him. The managers loved him and he could dominate any development conversation. The people working around him said he left a trail of destruction behind him. I was told that I was the first person that could understand his projects. Maintainability matters. I care most about TCO. For some firms, that's what matters.
I logged into Github after not being there for a while and for some reason it took me to an article in a repository by someone that seemed random. I was thinking "what is this" and had some trouble getting to my home page, so I read it. I didn't really register it at the time, but it was amazing. Every developer should read it. It largely said that almost everything we've been told about programming best practices leads to excessive "cognitive load", meaning our minds are getting kicked by the intellectual demands. I've known this for a while, especially with the demands of cloud, security and DevOps.
I also liked it because it described practices I have done for decades, but never much admit to because they are not popular... I write really complicated stuff and need all the help I can get.
Consider, if I'm right, it popped up because the Github folks, very smart people, though that developers should see it. I agree.







