03 - KISS (Keep It Simple, Stupid)
video: https://youtu.be/... no frontmatter da nota.KISS (Keep It Simple, Stupid)
KISS é o princípio de design que manda escolher, entre as soluções que resolvem o problema,
a mais simples que atende os requisitos atuais. A origem é a indústria aeronáutica: Kelly
Johnson, do Skunk Works da Lockheed, exigia projetos tão simples que um técnico de campo com
ferramentas básicas conseguisse reparar a aeronave. Em software de produção, a tradução é
direta: o sistema precisa ser operável por quem está de plantão às três da manhã, com o
runbook em mãos e sem depender do conhecimento tribal do autor original. Complexidade não é
incômodo estético; ela se paga em incidentes mais longos, onboarding mais lento e mudanças
mais arriscadas. O princípio não pede código trivial, pede que cada unidade de complexidade
adicionada compre um benefício correspondente e visível.
Complexidade essencial versus acidental
Fred Brooks separou a complexidade em duas espécies, e essa distinção é a base de qualquer
conversa honesta sobre simplificação. Complexidade essencial é a que pertence ao problema:
cálculo de impostos, regras de câmbio, concorrência real entre transações. Complexidade
acidental é a que o time escolhe introduzir: camadas extras, abstrações genéricas,
frameworks adotados sem necessidade e configurabilidade que ninguém usa. KISS ataca apenas a
segunda, porque a primeira não pode ser eliminada, apenas organizada. O erro comum é tratar
a essencial como se fosse acidental e tentar escondê-la atrás de abstrações, o que
normalmente aumenta o problema. Antes de simplificar, classifique a complexidade em jogo;
simplificar o essencial produz sistema incompleto, e tolerar o acidental produz sistema
caro.
Um exemplo concreto ajuda a calibrar o olhar. Um serviço de assinaturas com regras fiscais
por estado carrega complexidade essencial: as regras existem fora do software e não há como
removê-las. O time, porém, acrescentou estratégias registradas por arquivo de configuração,
um motor de regras genérico e três níveis de mapeamento entre entidades; nenhuma dessas
peças responde a requisito fiscal, todas respondem a uma hipótese de flexibilidade. Seis
meses depois, corrigir uma alíquota exigia tocar em quatro arquivos e reexecutar a suíte
inteira. A regra essencial continuou complexa; a acidental dobrou o preço de lidar com ela.
Separar os dois tipos nesse diagnóstico é o que permite simplificar sem cortar requisitos.
O custo da complexidade em produção
Cada componente adicional traz modos de falha próprios, e o custo aparece em três contas
mensuráveis. A primeira é o tempo médio de restauração: quanto mais peças móveis, mais tempo
para localizar a origem de um incidente e mais superfícies para inspecionar. A segunda é o
custo de entrada: um time que leva semanas para entender o fluxo de um pedido gera custo
salarial em cada contratação e risco em cada troca de equipe. A terceira é o custo de
mudança: complexidade composta se multiplica, porque cada nova feature precisa atravessar
todas as camadas e respeitar todas as exceções acumuladas.
Times maduros monitoram proxies de complexidade e tratam crescimento sem justificativa como
problema técnico de verdade. Tamanho médio de pull request, número de dependências diretas,
quantidade de configurações ativas e número de componentes no caminho crítico são métricas
simples de coletar. Quando esses indicadores sobem sem aumento correspondente de capacidade
entregue, a curva de custo da complexidade já está dominando a curva de valor.
Over-engineering e seus formatos mais frequentes
O over-engineering tem formatos previsíveis, e reconhecê-los acelera a revisão de
arquitetura. O primeiro é a solução genérica para um caso único: framework de plugins quando
existe um único plugin possível. O segundo é a arquitetura antecipada: microserviços, filas
e cache para um produto com trinta usuários e um requisito de consistência simples. O
terceiro é a configurabilidade infinita: sistemas que permitem ajustar o que nunca foi
ajustado, pagando o custo de testar todos os caminhos. O quarto é a indireção em cascata:
camadas que apenas repassam chamadas, DTOs espelhando entidades e mapeadores sem
transformação. O quinto é a adoção de tecnologia sofisticada por prestígio, como event
sourcing para um domínio sem auditoria relevante. Todos têm algo em comum: cobram juros
diários e pagam retorno incerto.
Como simplificar na prática
Simplificação é trabalho de engenharia, e não apenas de intenção. Comece pela pergunta de
escopo: qual requisito real este componente atende hoje, e qual atenderia apenas
hipoteticamente? Remova o que só tem resposta hipotética, com registro em ADR para permitir
reversão segura. Prefira tecnologia chata e consolidada, com boa documentação e comunidade
grande, porque o custo de operar com pessoas disponíveis no mercado é menor. Escolha a
ferramenta menos poderosa que resolve o problema: um cron com SQL resolve lote noturno de
dez mil linhas sem cluster de streaming.
O restante é disciplina operacional. Colapse camadas que não fazem transformação, unifique
configurações duplicadas e delete código morto com a mesma seriedade com que se escreve
código novo. Mantenha o caminho feliz simples e isole a exceção, em vez de espalhar
condicionais pelo fluxo principal. Por fim, trate simplificação como item de backlog com
prioridade: "podemos remover isso?" é pergunta legítima em revisão de arquitetura, e o
trabalho de remoção compete por capacidade como qualquer outro.
Exemplo prático: a mesma necessidade, dois custos
O cenário: processar diariamente um arquivo de conciliação com dez mil linhas. A primeira
versão, montada para escalar, tinha produtor na fila, três consumidores com controle de
concorrência, reprocessador de mensagens mortas e dashboards dedicados; o custo de operação
e o tempo de diagnóstico eram maiores que o próprio processamento. A versão simplificada
mantém o mesmo contrato de saída:
# Versão simples: job noturno com retry idempotente e log estruturado.
# O lote de 10 mil linhas roda em minutos; não há necessidade de streaming.
def conciliar_dia(data: date) -> RelatorioConciliacao:
linhas = baixar_arquivo(data) # fonte oficial do dia
falhas: list[ErroConciliacao] = []
for linha in linhas:
try:
registrar_lancamento(linha) # idempotente por chave única
except ErroNegocio as e:
falhas.append(e) # fila morta vira lista com dono
return RelatorioConciliacao(data, falhas) # alerta só quando há falha
A versão simples tem menos peças para falhar, diagnóstico direto e capacidade de retorno
clara quando o volume crescer dez vezes. Nesse momento, a troca por processamento
distribuído é uma decisão com dados de produção, e não uma aposta antecipada. O mesmo
raciocínio vale para cache, fila e particionamento: cada um entra quando a medição pedir, e
o desenho simples de hoje não deve dificultar essa entrada.
Simplicidade como decisão arquitetural
KISS precisa estar institucionalizado nas decisões, ou ele perde para o entusiasmo de cada
sprint. Registre em ADRs a razão de cada componente existir, com o requisito que justifica o
custo; requisito que sumiu vira candidato à remoção. Inclua simplicidade como critério
explícito em revisão de arquitetura e em code review, com perguntas do tipo: existe caminho
mais curto que atende o requisito atual? Cada peça nova traz qual modo de falha adicional? O
time de plantão consegue operar isso sem o autor?
Orçamentar complexidade completa o processo. Limite o número de tecnologias por domínio,
exija justificativa formal para adicionar mais uma e revise a lista periodicamente.
Simplicidade não é ausência de sofisticação; é sofisticação suficiente para o problema, e
nada além disso. A frase "make it work, make it right, make it fast" resume a ordem correta:
funcionar primeiro, com estrutura limpa, e acelerar só o que a medição apontar.
Erros comuns e boas práticas
- Confundir simples com fácil: a solução simples pode exigir esforço intelectual maior agora, e é exatamente esse investimento que reduz o custo futuro.
- Simplificar a complexidade essencial: cortar regra de negócio ou tratamento de concorrência real produz sistema barato e errado.
- Medir simplicidade por linhas de código: menos linhas com regex densa pode ser menos simples; métrica melhor é número de conceitos e de peças móveis.
- Refatorar para simplificar sem testes: mudanças estruturais amplas exigem rede de proteção; comece pelos testes de caracterização.
- Ignorar o custo de migração: trocar componentes simples por outros igualmente simples tem custo; simplifique primeiro o que dói na operação.
- Tratar KISS como veto a tecnologia nova: a adoção é legítima quando o benefício compensa a complexidade; a exigência é justificar o acréscimo.
Antipadrões e alternativas
A tabela reúne os antipadrões de complexidade acidental mais comuns, o custo que geram em
produção e a alternativa simples correspondente.
| Antipadrão | Custo em produção | Alternativa simples |
|---|---|---|
| Fila de mensagens para lote noturno | Alertas, DLQ e dashboards extras | Cron com processamento idempotente |
| Framework de plugins com um plugin | Superfície de API morta | Chamada direta e ponto de troca futuro |
| Microsserviços por entidade | Latência, orquestração e custo de ops | Módulos internos em monólito bem traçado |
| Configuração ajustável sem consumidor | Matriz de combinações não testadas | Constantes em código e ADR para mudar |
| Camadas de repasse sem transformação | Rastro de depuração mais longo | Menos camadas, com responsabilidade real |