Você abre o repositório de um projeto novo na sexta de manhã. Está vazio: um README.md, um .gitignore, um arquivo de solução em branco. O ticket diz que o time tem três meses pra entregar a primeira versão de um produto que ainda está sendo desenhado pelo time de negócio, e que precisa de uma API REST com "uns oito endpoints, pode ser que vire mais".
Você respira, abre um post recente no LinkedIn elogiando Clean Architecture, lê de novo a parte sobre use cases e ports & adapters, estrutura de pastas para DDD, e antes de entender o domínio já decidiu a como vai organizar o código do projeto.
Vinte minutos depois, o projeto tem Domain, Application, Infrastructure, Presentation, três bibliotecas de classe, um pacote do MediatR adicionado, AutoMapper e um arquivo IRepository<T> ainda vazio porque você não decidiu se vai usar Dapper ou EF Core. Você sente que fez a coisa certa. O projeto, na sua cabeça, agora tem "fundação".
Venho aqui defender a tese contrária. A decisão de arquitetura que você acabou de tomar é, com altíssima probabilidade, a errada. Não porque Clean Architecture seja ruim, mas porque foi tomada antes de o problema existir. E o custo dessa decisão vai aparecer daqui a seis meses (não amanhã) exatamente quando ela ficar cara demais pra ser desfeita sem dor.
A tese central é simples de falar: padrão de arquitetura nasce da necessidade, não da estética. O resto do artigo é só a defesa dessa tese, com casos, código, e algumas ressalvas pra que ninguém ache que eu estou pregando contra rigor técnico.
Padrões de arquitetura bem conhecidos
Vale citar alguns nomes que dominam a discussão pública hoje, e que provavelmente ocorreram pra você quando leu o parágrafo de abertura. Nenhum deles é propriedade de uma linguagem: você encontra todos em .NET, Java, Node, Python, Go, Kotlin ou qualquer outra.
Clean Architecture, formalizada por Robert C. Martin em 2012, organiza o sistema em camadas concêntricas onde as dependências sempre apontam pra dentro. Regras de negócio ficam no centro, frameworks na borda, e a inversão de dependência garante que o domínio nunca conhece o detalhe técnico. Ela brilha em sistemas com lógica de negócio densa (seguros, banking, saúde), times grandes, integrações externas pesadas, e onde testabilidade alta é requisito de regulação.
Hexagonal, proposta por Alistair Cockburn em 2005, tem a mesma intuição central: isolar o núcleo da aplicação do mundo externo, atravessando essa fronteira por meio de ports & adapters. Resolve o mesmo problema de Clean com vocabulário diferente, e brilha quando há troca frequente de infraestrutura (banco, fila, broker) ou múltiplos canais de entrada (REST, gRPC, CLI, fila).
Vertical Slice, popularizada por Jimmy Bogard por volta de 2018, é filosoficamente o oposto de Clean: em vez de compartilhar abstrações entre camadas, organiza o sistema por feature, e duplica de propósito quando dois slices parecem fazer coisas parecidas. Brilha em sistemas com muitas funcionalidades pouco acopladas e times grandes trabalhando em paralelo, e fracassa miseravelmente quando as features dependem demais umas das outras, virando um projeto "shared" gigante que ninguém ousa tocar.
Repare que esses três nomes são padrões de arquitetura de dentro de uma aplicação: camadas, dependências, organização de pastas. Mas existe uma segunda escala, que também entra nessa discussão, que decide como o sistema se divide em partes que rodam separadas: um monolito, um monolito modular, vários microsserviços, etc.
As duas escalas são a mesma pergunta vista de distâncias diferentes, e a tese desse artigo vale igual nas duas. Vou transitar entre elas ao longo do texto de propósito, porque o erro é o mesmo dos dois lados.
Cada uma dessas opções é boa no contexto certo. Esse contexto é o tema do artigo.
Qual usar?
Aqui está a pergunta natural depois de apresentar esses exemplos: qual deles você deve usar no seu próximo projeto?
A minha resposta, sem retórica de palestra, é: nenhuma.
Calma, não é que arquitetura não importa. É que a decisão de qual arquitetura usar, feita no dia zero, antes de entender o problema, antes de validar o produto, antes de ter dor real, é quase sempre a decisão errada.
Overengineering nasce justamente desse impulso de "fazer bonito desde o começo". A escolha precoce de arquitetura é, na maior parte dos casos, uma decisão estética disfarçada de decisão técnica.
E a primeira coisa que precisamos separar pra que essa tese não vire desculpa pra fazer porcaria é:
Código limpo e arquitetura limpa são coisas diferentes
Existem duas coisas que costumam ser tratadas como uma só, e essa confusão é responsável por boa parte do overengineering que eu vejo.
Código limpo é sobre a qualidade do código em si: como você escreve cada função, cada classe, cada bloco lógico. Nomes claros, funções pequenas, responsabilidades bem definidas, ausência de gambiarras "espertas".
É uma disciplina linha a linha, e na minha opinião deveria ser obrigatório sempre. Não importa se o projeto é um MVP de hackathon, um sistema interno de oito endpoints, ou um produto bem-sucedido em produção. Código limpo não tem desconto.
Arquitetura limpa é sobre como o sistema está organizado, e isso acontece nas duas escalas que eu mencionei: de perto, como você arruma o código dentro de uma aplicação (camadas, dependências, slices, que é onde entram Clean, Hexagonal e Vertical Slice); e de longe, como você divide o sistema em partes que rodam separadas (monolito, monolito modular, minisserviços, microsserviços).
É uma disciplina estrutural, e é contextual nas duas pontas: depende do problema, do time, do momento. E o mesmo erro se repete nas duas escalas, criar cinco camadas pra um CRUD é o mesmo equívoco que quebrar em quinze microsserviços um produto que cabia tranquilo num monolito.
Código limpo é inegociável. Arquitetura limpa é contextual. Não confundir os dois é o primeiro passo pra parar de se enganar.
Quando alguém diz "ah, então eu posso fazer qualquer coisa de qualquer jeito, né?", essa pessoa está confundindo os dois eixos. O código sempre tem que ser limpo. A arquitetura é uma decisão de engenharia que responde a um custo, e que portanto só faz sentido se houver um custo correspondente pra pagar.
O que é overengineering?
Overengineering é fazer mais engenharia do que o problema pede. Não é sobre fazer bem feito. É sobre fazer demais.
Criar use cases, boundaries, entities e adapters quando você só precisa de meia dúzia de operações básicas gera código mais complexo, onboarding mais difícil, velocidade desnecessariamente menor nas entregas (especialmente nas primeiras) e camadas que não resolvem nenhuma dor real. O time fica preso na arquitetura, no "como", em vez de focar no problema.
Tanta "robustez" deixa o sistema frágil pela complexidade desnecessária. Esse é o paradoxo central do overengineering, e é o motivo de ele ser tão difícil de combater: ele se parece com profissionalismo.
Sintomas clássicos (com código)
O melhor jeito de reconhecer overengineering é olhando código. Vou pegar dois sintomas que aparecem com frequência embaraçosa em projetos novos.
Sintoma 1: CQRS num CRUD simples
Imagine o cenário mais comum do mundo: você precisa de um endpoint pra cadastrar cliente. Recebe um JSON com nome, e-mail e CPF, salva no banco, devolve o ID. Eis como muita gente faria isso "profissionalmente" hoje:
// 6 artefatos para inserir um Cliente
public record CreateClienteCommand(string Nome, string Email, string Cpf);
public class CreateClienteCommandValidator : AbstractValidator<CreateClienteCommand>
{
public CreateClienteCommandValidator() { /* regras */ }
}
public class CreateClienteCommandHandler
{
private readonly IClienteRepository _repo;
private readonly IMapper _mapper;
public async Task<CreateClienteResult> Handle(CreateClienteCommand cmd, CancellationToken ct)
{
var cliente = _mapper.Map<Cliente>(cmd);
await _repo.AddAsync(cliente, ct);
return _mapper.Map<CreateClienteResult>(cliente);
}
}
public record CreateClienteResult(Guid Id, string Nome, string Email);
public class ClienteMappingProfile : Profile { /* perfis */ }
// + Controller, DTO de entrada, registro no container DI, etc.
Seis artefatos pra fazer um INSERT. Em alguns sistemas (grandes, com 200 use cases, validação rica, comportamento cross-cutting via pipeline), esse é o caminho certo, e eu não estou atacando CQRS. Mas se você tem oito endpoints de uma área administrativa interna, isso é overengineering puro.
Veja a versão suficiente:
[ApiController]
[Route("api/clientes")]
public class ClientesController : ControllerBase
{
private readonly AppDbContext _db;
public ClientesController(AppDbContext db) => _db = db;
[HttpPost]
public async Task<IActionResult> Create(CreateClienteRequest req)
{
if (string.IsNullOrWhiteSpace(req.Nome) || string.IsNullOrWhiteSpace(req.Email))
{
return BadRequest();
}
var cliente = new Cliente
{
Id = Guid.NewGuid(),
Nome = req.Nome,
Email = req.Email,
Cpf = req.Cpf
};
_db.Clientes.Add(cliente);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(Create), new { id = cliente.Id }, cliente);
}
}
Menos de 20 linhas. Separação entre Request e Entity continua existindo. Nomes continuam claros. Tratamento de erro continua explícito. Nenhuma mágica. Simplicidade não é bagunça. É o ponto principal desse texto.
Esse não é um exemplo de laboratório, eu já vivi ele. Numa época em que eu era tech lead, a gente pegou um projeto pequeno pra um cliente grande: um piloto, antes de pousar, solicitava por um app que o caminhão de reabastecimento já estivesse esperando o avião na vaga. No fundo era um CRUD, criar um pedido de uma ponta, listar os pedidos da outra. Designamos um dev, falamos "é simples, qualquer coisa me chama" e, confesso, não acompanhei como devia, então a culpa boa parte foi minha.
Quando eu olhei, o que era pra ser um INSERT tinha virado CQRS, use cases, command handlers, banco de leitura separado do de escrita. Estourou o prazo, o front ficou de lado e nem ficou bom pro usuário, e o pior veio depois: o dev saiu pra outro projeto e o substituto, fazendo o handover, vinha me procurar meio desesperado dizendo que não conseguia entender se o que estava no card era verdade, de tão emaranhado que estava o código.
Conceitos bons, contexto errado. É exatamente assim que a armadilha pega gente competente.
Sintoma 2: Strategy sem dor real
Outro exemplo: cálculo de frete. Acima de R$ 200, é grátis; abaixo, R$ 20. Lado A, o que muita gente entrega:
public interface IFreteStrategy
{
decimal Calcular(decimal valor);
}
public class FreteGratisStrategy : IFreteStrategy
{
public decimal Calcular(decimal v) => 0m;
}
public class FreteFixoStrategy : IFreteStrategy
{
public decimal Calcular(decimal v) => 20m;
}
public class FreteFactory
{
public IFreteStrategy Criar(decimal v) =>
v > 200m ? new FreteGratisStrategy() : new FreteFixoStrategy();
}
Quatro classes, uma interface, uma factory. Pra duas regras fixas. Lado B, a versão honesta:
public class PedidoService
{
public decimal CalcularFrete(decimal valorPedido)
=> valorPedido > 200m ? 0m : 20m;
}
Uma linha. Strategy é ótimo quando você tem muitas estratégias (Correios, transportadora X, transportadora Y, retirada em loja, frete internacional). Pra duas regras fixas, o ternário é o código profissional, e o Strategy é arquitetura tentando se justificar.
Por que caímos na armadilha
Vale entender por que pessoas competentes, com boa intenção, repetem esse padrão. Eu vejo cinco motivos, e nenhum deles é incompetência.
O senior traumatizado. Quem já apanhou de sistema mal arquitetado tende a superprojetar o próximo, e o trauma vira excesso. É uma reação compreensível, e por isso difícil de notar em si mesmo.
Currículo e status. "Eu uso Clean + CQRS + Event Sourcing" soa melhor numa entrevista do que "eu fiz um monolito modular bem feito". A linguagem técnica que vende é a da sofisticação, mesmo quando a entrega que cria valor é a da simplicidade.
Conteúdo da internet. Posts e cursos vendem sofisticação, porque ninguém viraliza fazendo CRUD simples. O algoritmo recompensa diagrama bonito, e diagrama bonito quase nunca é o do sistema que está em produção dando dinheiro.
Medo de errar pra menos. Parece mais seguro projetar pro futuro do que pro presente, porque "se o sistema crescer e eu não tiver feito assim, eu fui amador". A inversão dessa lógica (se o sistema crescer e eu tiver feito assim sem precisar, eu desperdicei meses) raramente é considerada.
(e talvez o pior) Confusão com profissionalismo. Tem dev que acha que código simples é amador. É exatamente o contrário: escrever simples é o que dá trabalho.
Overengineering raramente vem de má intenção. Vem do impulso de fazer bonito desde o começo, e esse impulso é, paradoxalmente, anti-profissional.
Eu gosto de uma analogia simples pra explicar isso. Imagine alguém com medo de ter câncer no futuro: pesquisa na internet, toma um monte de vitamina, suplemento, remédio, tudo pra se blindar de uma doença que talvez nunca venha. E, no meio dessa preocupação com o futuro distante, esquece de medir a glicose, ignora os sinais do dia a dia, e desenvolve diabetes.
Foi cuidar do problema hipotético e deixou passar o real, o que estava ali na frente, dando sinal. Arquitetura é a mesma coisa: não se blinde contra a escala que talvez nunca chegue ao ponto de não enxergar a dor que o sistema já está sentindo hoje.
Casos reais
Os sintomas acima foram da arquitetura "de perto", dentro do código. Agora subo um nível pra escala "de longe", a de dividir o sistema em serviços, porque é exatamente o mesmo erro com outro figurino, e o caso a seguir mostra as duas coisas acontecendo juntas.
Só que aqui o custo do exagero é bem maior, e vale torná-lo concreto, porque ele costuma ser invisível no dia da decisão. No CRUD, o preço do overengineering eram seis classes a mais. Ao quebrar um sistema em microsserviços antes da hora, o preço é outro: o que era uma chamada de função vira uma chamada de rede, que pode falhar, ter latência e precisar de retry. O que era uma transação de banco vira consistência eventual, e agora você precisa lidar com estados intermediários e outbox, saga, compensação.
Depurar um fluxo deixa de ser um stack trace e passa a exigir tracing distribuído pra entender por onde a requisição passou. Um deploy simples vira coreografia de versões e contratos entre serviços. Nada disso é exótico: é o custo-base de qualquer sistema distribuído, e você paga ele inteiro mesmo que o problema não tivesse pedido distribuição nenhuma.
Eu vou contar uma história, sem nomear a empresa, relatado por um amigo próximo. Uma startup que cresceu rápido. Em 2019, time pequeno, urgência de entregar. O pessoal leu meia dúzia de posts no LinkedIn, achou bonito, e decidiu aplicar Clean Architecture desde o dia zero. Em tudo. Inclusive em serviços de quatro endpoints. E como já estavam "fazendo bonito", quebraram o sistema em microsserviços na mesma pegada.
Em 2024, o que sobrou:
- 20+ microsserviços para um produto que precisava de quatro
- 90+ dias de onboarding pra cada dev novo
- No mínimo, 6 serviços diferentes pra responder uma única requisição
- Repositórios duplicados, sem que ninguém saiba qual é o "oficial"
Repare que a Clean Architecture e os microsserviços foram a mesma decisão, tomada pelo mesmo motivo estético, no mesmo dia zero. E isso é caro, porque desfazer overengineering é tão caro quanto fazer. Overengineering bem-intencionado é o mais perigoso, porque te custa caro duas vezes: uma quando você implementa, outra quando você precisa desfazer.
Antes que pareça que esse é um problema só de startup que não sabe se virar, vale olhar três casos públicos, em ordem crescente de tamanho. Todos giram em torno da mesma decisão de dividir ou não dividir o sistema em serviços, que é onde o custo do exagero fica mais visível.
Basecamp / DHH roda um produto multi-milionário em monolito Rails há mais de vinte anos, e o DHH escreveu textos famosos defendendo a tese do "Majestic Monolith". O argumento dele é direto: a complexidade dos microsserviços só compensa quando há escala humana e técnica que justifiquem o custo.
Stack Overflow atendia bilhões de requisições por mês em um monolito enxuto, sem microsserviços, sem service mesh, sem nada do que costuma se vender em conferência. Nick Craver tem posts famosos descrevendo essa arquitetura, e a parte mais chocante é o quanto ela é, de fato, simples.
Amazon Prime Video, em 2023, publicou um artigo que virou meme da internet, então vale contar direito, porque quase todo mundo cita errado. Não foi "a Amazon abandonou microsserviços". Foi um time específico, o de análise de qualidade de vídeo (VQA), num único serviço, o de monitoramento de streams ao vivo. Esse serviço tinha sido montado com orquestração serverless (Step Functions, Lambdas, frames passando por S3 entre etapas) e bateu num teto de escala a 5% da carga esperada, ficando caríssimo.
A solução foi juntar as etapas num único processo em container (ECS), e isso cortou 90% do custo. A própria AWS depois fez questão de dizer que a lição não é "monolito é melhor que microsserviço", e sim "escolha a topologia certa pro problema certo". Que é exatamente a tese aqui: a empresa que mais entende de microsserviços no mundo recuou num pedaço onde a distribuição custava mais do que entregava.
Repare o padrão: empresas que podem se dar ao luxo da sofisticação escolhem a simplicidade quando ela serve. Quem geralmente cai na cilada do overengineering é quem ainda não tem escala, mas quer agir como se tivesse.
A evolução natural de software
Se a tese for "comece simples", a pergunta justa é: como o projeto evolui sem virar bagunça? A resposta é uma palavra antiga, fácil de citar, difícil de praticar.
YAGNI, ou You Ain't Gonna Need It, é uma das regras originais do Extreme Programming, formulada por Kent Beck nos anos 90. Não implemente nada baseado em uma necessidade futura especulada. Implemente quando a necessidade for concreta, atual, demonstrável.
O motivo de YAGNI funcionar é menos óbvio do que parece. Implementar antes da necessidade não tem só o custo de construir agora; tem três custos:
- Construir agora: tempo e dinheiro gastos numa coisa que pode nunca ser usada.
- Manter: essa coisa que existe e ninguém usa continua aparecendo em revisão de código, em métrica de cobertura, em refactoring de dependência. Ela pesa.
- Remover ou refatorar: quando o futuro vier diferente, e ele sempre vem diferente, você vai precisar tirar essa coisa do caminho, e isso custa mais do que ter resistido à tentação no início.
Esse "o futuro sempre vem diferente" não é força de expressão. Eu trabalho muito com integrações com o WhatsApp, e quem lida com a Meta sabe: num ano eles anunciam que aquele é o jeito definitivo de enviar e cobrar mensagens, você capricha numa estrutura robusta pensando nos próximos três anos, e seis meses depois eles mudam tudo de novo. Toda a engenharia que você antecipou pro futuro vira retrabalho. E isso tem preço, literalmente.
De uns anos pra cá eu passei a ter um CEO que liga a hora do desenvolvedor direto ao caixa: quantas horas esse produto custou, quanto a empresa precisa vender pra ter lucro com ele. Quando você enxerga assim, fica claro que tem hora que a gente gasta tempo entregando uma qualidade que o momento não pedia e esse tempo é dinheiro vazando justamente pelo time de engenharia.
YAGNI não é desculpa pra fazer mal feito. Faça bem feito, mas só o que é necessário agora.
Aqui cabe a ressalva mais importante do artigo, porque é a objeção honesta que todo sênior experiente vai levantar: "mas tem decisão que é cara demais pra desfazer depois". Verdade, e é uma distinção que muda tudo.
A Amazon usa a metáfora das portas de mão dupla e de mão única. A maioria das decisões é porta de mão dupla: se der errado, você volta. Trocar um ternário por um Strategy, extrair um serviço, introduzir uma camada, dá pra reverter num PR. Pra essas, YAGNI manda: decida tarde, decida barato, erre barato.
Mas algumas são porta de mão única, caras ou impossíveis de reverter depois que clientes e dados dependem delas: o contrato público da sua API, o modelo de dados central, a escolha de quebrar a base em vários bancos, a fronteira entre dois serviços que viram repositórios e times separados. Nessas poucas, vale pensar duas vezes e investir um pouco mais de cuidado no dia zero.
O erro não é pensar no futuro. O erro é tratar toda decisão como se fosse de mão única, e portanto enrijecer tudo "por garantia". A disciplina é justamente saber separar as duas: seja agressivamente simples nas decisões reversíveis, que são a esmagadora maioria, e reserve o peso da deliberação pras raras que não dá pra voltar atrás. Overengineering é, no fundo, tratar porta de mão dupla como se fosse de mão única.
Cada decisão arquitetural deve responder a uma dor real. Nenhuma deveria ser feita somente pelo amor à arte.
Quando arquitetura limpa vale a pena
Tudo isso pode soar como uma cruzada contra Clean, Hexagonal, Vertical Slice e microsserviços, e não é. Arquitetura limpa é cara no início e barata no longo prazo, mas só quando o longo prazo existe.
| Vale quando... | Não vale quando... |
|---|---|
| O domínio é complexo e central (banking, seguros, saúde, regulação) | Você está validando produto ou MVP |
| O time é grande e muitas pessoas tocam no código | Não tem certeza sobre o escopo |
| O sistema vai existir por anos | Ainda não entendeu as regras de negócio |
| Há muitas integrações externas | Não sabe se o sistema vai crescer |
| Testabilidade é requisito (regulação, contrato) | O domínio ainda está sendo descoberto |
Se você lê a coluna da direita e reconhece o seu projeto atual, pare de tentar resolver com estrutura o que ainda é problema de descoberta. Estrutura não substitui entendimento de domínio; ela apenas torna a confusão mais cara de desfazer.
Como decidir na prática
Tudo que eu disse até aqui é conceitual. Aqui vai o aterramento: como decidir, na segunda-feira de manhã, no projeto de vocês.
O mínimo profissional
Quando eu defendo "comece simples", não estou defendendo "comece sem nada". Existe um conjunto de fundações que entra em qualquer projeto, independente do tamanho, e que não tem desculpa pra ficar pra depois:
- Código limpo e idiomático
- Estrutura mínima por feature
- Versionamento decente (Git, com mensagens que outro humano consegue ler)
- Testes do que dói (talvez não cobertura total, mas o suficiente pra dormir tranquilo)
- Logs estruturados
- Observabilidade básica
- Configuração externalizada
- CI/CD mínimo
Nada disso é Clean, Hexagonal, Vertical Slice ou microsserviços. É fundação básica de profissionalismo. Não tem desculpa pra começar projeto sem isso, e ter isso já te dá uma posição muito mais saudável do que ter Clean Architecture sem ter testes.
Quando subir de nível
Duas perguntas guiam isso, e elas são complementares: uma olha pra frente ("estou pronto?"), a outra olha pro presente ("o sistema já está pedindo?").
A primeira é o teste de prontidão, antes de adotar qualquer arquitetura mais elaborada (Clean, Hexagonal, microsserviços).
- O domínio está claro?
- O escopo é estável, ou ainda há pivotagem provável?
- O time vai crescer ao ponto de precisar de fronteiras?
E principalmente: existe um problema concreto que essa arquitetura resolve, que você consiga descrever em uma frase?
Se você não consegue, não está pronto, e "ainda não" é diferente de "nunca".
A segunda pergunta é sobre os sintomas. Você não precisa adivinhar a hora, o sistema avisa:
- mexer numa feature simples passou a tocar cinco arquivos espalhados
- onboarding de dev novo demora demais; os times vivem em conflito de merge
- os testes ficaram lentos ou frágeis; trocar de tecnologia virou épico de backlog
- a regra de negócio está espalhada por controllers, services e helpers
Dois ou três desses juntos ao mesmo tempo? Pare de adiar, porque a partir daí o custo de não investir supera o de investir, e a sua "simplicidade sustentável" virou negligência. E note que evoluir não é reescrever do zero: você isola o módulo que dói, extrai o serviço que precisa existir, introduz a camada onde ela ganha o seu sustento, um passo de cada vez.
A regra final
Se você esquecer tudo desse artigo menos uma coisa, lembre disso:
Resolva o problema de hoje com a estrutura mais simples que você consegue manter limpa. Quando o problema mudar, a estrutura te avisa e você a evolui.
É o sistema que te diz "tá na hora de evoluir". Não é um livro, não é uma palestra, não é a moda do mês. É a dor real do dia a dia. Escute essa dor.
Gostou do artigo? Comente abaixo sobre o que ele te fez pensar e que práticas você deseja aplicar. Além disso, comente sobre o que faltou no artigo que é informação importante sobre o assunto
Top comments (0)