O If de Par e Ímpar Disfarçado de Arquitetura
Você conhece aquele exercício de programação iniciante: dado um número, diga se é par ou ímpar. A solução ingênua que todo mundo ri:
if (n == 0) print("par")
if (n == 1) print("ímpar")
if (n == 2) print("par")
if (n == 3) print("ímpar")
if (n == 4) print("par")
...
Ninguém faria isso. É óbvio que a solução é n % 2 == 0. Uma regra genérica que resolve todos os casos sem enumerar cada um. Qualquer dev olha esse código e diz: "isso é absurdo, você está codificando cada caso individual quando existe um padrão".
Agora abre qualquer backend corporativo de relatórios e me diz o que você vê.
Antes de continuar: esse artigo fala especificamente de backends de leitura. Dashboards, relatórios, consultas. Endpoints que recebem parâmetros, executam uma query e devolvem dados. Se o seu contexto envolve escrita, mutação de estado, regras de negócio complexas ou side effects, a conversa é outra. Vamos chegar lá.
Guia de tópicos:
- O Exercício de Par e Ímpar em Produção
- Como Centenas de Ifs Se Disfarçam de Arquitetura
- O Polimorfismo Que Não Polimorfiza Nada
- A Multiplicação das Condicionais por Camada
- O Que Seria o
n % 2Desse Problema - Os Trade-offs Que Você Vai Herdar
- E Quando o Endpoint Não É Leitura Pura?
- Por Que Ninguém Percebe (E Por Que Não É Culpa de Ninguém)
- Considerações Finais
- Links Indicativos
O Exercício de Par e Ímpar em Produção
Um backend de dashboards típico tem centenas de endpoints. Cada endpoint faz a mesma coisa:
- Recebe request
- Valida parâmetros
- Verifica cache
- Executa consulta no banco
- Retorna JSON
O que muda entre um endpoint e outro? A URL e qual consulta executa. Só. O pipeline é idêntico. A validação é a mesma. O cache funciona igual. O tratamento de erro é o mesmo.
Mas em vez de expressar isso como um padrão com variações, o projeto expressa como centenas de implementações individuais. Cada endpoint tem sua própria classe. Cada classe tem seu próprio método. Cada método faz exatamente a mesma coisa que os outros, trocando apenas o nome da consulta que chama.
Isso é o if de par e ímpar. Literalmente. É enumerar cada caso individual quando existe um padrão óbvio por baixo.
if (url == "/vendas/pedidos/total") → executa consultaA
if (url == "/vendas/pedidos/monitoramento") → executa consultaB
if (url == "/logistica/entregas/total") → executa consultaC
if (url == "/logistica/entregas/pendentes") → executa consultaD
if (url == "/financeiro/faturas/abertas") → executa consultaE
...centenas de vezes
Como Centenas de Ifs Se Disfarçam de Arquitetura
O truque é que ninguém escreve esses ifs explicitamente. Eles estão disfarçados de boas práticas:
Disfarce 1: Classes com nomes bonitos. Em vez de if (url == X), você tem SalesOrderService, LogisticsShipmentService, FinanceInvoiceService. Cada classe é um if. A existência da classe é a condição. O nome da classe é o predicado. O corpo do método é o bloco de execução.
Disfarce 2: Injeção de dependência. O container registra cada service individualmente. container.register('ISalesOrderService', () => new SalesOrderService(repo)). Isso é um mapa de condições: dado o nome, retorne a instância. É um switch/case com syntax sugar.
Disfarce 3: Interfaces. ISalesOrderService, ILogisticsShipmentService, IFinanceInvoiceService. Cada interface declara os mesmos métodos com as mesmas assinaturas. A única diferença é o nome. São ifs tipados.
Disfarce 4: Rotas declarativas. Cada rota é registrada individualmente com sua lista de middlewares. router.post('/vendas/pedidos/total', [auth, validate, cache, service.getTotal]). Parece declarativo. Mas quando você tem centenas dessas declarações idênticas variando só a URL e o último handler, é uma tabela de ifs.
O resultado: um projeto com centenas de classes, centenas de interfaces, centenas de registros no container, centenas de definições de rota. Todas fazendo exatamente a mesma coisa. É o if (n == 4) print("par") vestido de terno.
O Polimorfismo Que Não Polimorfiza Nada
Polimorfismo serve para quando comportamentos são diferentes mas compartilham uma interface. Um Shape com draw() faz sentido porque Circle.draw() e Square.draw() têm implementações fundamentalmente diferentes.
Mas quando você tem SalesOrderService.getTotal() e LogisticsShipmentService.getTotal() e ambos fazem:
try {
var result = this.repository.getTotal(ids, filters);
return ok(result);
} catch (Exception e) {
return handleError(e);
}
Isso não é polimorfismo. É repetição. A "variação" entre as implementações é zero. O que varia é qual repositório está injetado, e isso é configuração, não comportamento.
É como criar uma classe ParChecker2, ParChecker4, ParChecker6, ParChecker8, cada uma retornando "par", e chamar isso de "polimorfismo". Tecnicamente é. Praticamente é absurdo.
A Multiplicação das Condicionais por Camada
O pior é que a arquitetura em camadas não esconde o problema. Ela o multiplica. Cada camada repete a decisão "qual variante sou eu?":
Camada de Rota: if url == X → use ServiceX
Camada de Service: if chamado → use RepositoryX
Camada de Repository: if chamado → use QueryX
Camada de Query: if chamado → retorne SQL_X
São 4 ifs implícitos para cada endpoint. Multiplicado por centenas de endpoints, são centenas de condicionais em cada camada. O projeto inteiro é uma árvore de decisão gigante onde cada folha faz a mesma coisa. Só muda qual SQL executa no final.
E quando surge uma variação real, tipo "comparar unidade contra regional vs. contra grupo vs. contra outra unidade", a árvore explode. Agora cada folha tem 3 sub-folhas. Cada sub-folha precisa de sua própria classe, sua própria interface, seu próprio registro. 3 métricas × 3 comparações = 9 classes novas. Que fazem quase a mesma coisa. Que diferem em qual tabela fazem JOIN.
Isso é o equivalente de:
if (n == 2 && comparando_com == "regional") print("par regional")
if (n == 2 && comparando_com == "grupo") print("par grupo")
if (n == 2 && comparando_com == "unidade") print("par unidade")
Quando a solução seria:
resultado = calcular(n)
label = resolver_comparacao(comparando_com)
print(resultado + " " + label)
O Que Seria o n % 2 Desse Problema
O n % 2 de um backend de leitura é: um engine que recebe configuração e executa o pipeline.
Em vez de centenas de classes que instanciam o mesmo pipeline manualmente, você tem:
- Uma tabela de endpoints (URL → query → middlewares)
- Um engine que lê a tabela e monta o pipeline
- As queries SQL (a lógica real)
- Os middlewares (poucos, reutilizáveis)
endpoints:
- path: /vendas/pedidos/total
query: vendas.pedidos.getTotal
- path: /vendas/pedidos/monitoramento
query: vendas.pedidos.listMonitoramento
pagination: true
- path: /logistica/entregas/total
query: logistica.entregas.getTotal
Cada linha é um endpoint. O engine sabe que todo endpoint passa por auth, validação, cache (a menos que diga o contrário). Sabe que pagination: true adiciona limit/offset. Sabe como tratar erros. Sabe como serializar.
Isso é o n % 2. Uma regra que resolve todos os casos. Não enumera cada número. Descreve o padrão.
As centenas de classes desaparecem. As centenas de interfaces desaparecem. Os centenas de registros no container desaparecem. O que sobra é: a tabela de configuração + as queries SQL + o engine. A lógica real (SQL) continua existindo. A cerimônia (classes/interfaces/wiring) some.
Os Trade-offs Que Você Vai Herdar
Seria desonesto apresentar o engine como solução sem falar do que você perde. Toda abstração tem custo, e essa não é exceção.
Debugging fica menos óbvio. Quando cada endpoint é uma classe, o stack trace te leva direto ao ponto. Com um engine genérico, o stack trace te mostra o engine, não qual configuração disparou o problema. Você troca "onde está o código?" por "onde está a configuração que gerou esse comportamento?". Isso exige tooling: logs estruturados com o path do endpoint, tracing que correlaciona a request com a entrada na tabela de configuração, mensagens de erro que digam qual YAML gerou o problema.
Erros migram de compilação para runtime. Um typo no nome de uma query em código tipado é pego pelo compilador. Um typo no YAML só aparece quando alguém faz a request. Isso significa que você precisa de validação na inicialização: o engine deve carregar a configuração no boot, verificar que todas as queries referenciadas existem, e falhar rápido se algo estiver inconsistente. Sem isso, você está trocando segurança de tipo por surpresas em produção.
O engine precisa de dono. Alguém vai manter esse engine. Alguém vai decidir como ele evolui quando surgir um caso que não se encaixa no modelo. Se o engine vira um projeto abandonado internamente, você acaba com o pior dos dois mundos: uma abstração que ninguém entende e que ninguém pode estender. O engine precisa de ownership clara, documentação, e uma política explícita para quando um endpoint não se encaixa no padrão (resposta curta: esse endpoint vira código normal, fora do engine).
Nem todo endpoint cabe. Vai existir o endpoint que precisa de um middleware específico, uma transformação de resposta diferente, uma validação contextual. O engine precisa de escape hatches, formas de dizer "esse caso é especial, trata diferente". Se o engine não tem isso, ele vira uma prisão. Se tem demais, vira o mesmo caos que tentou resolver.
A proposta não é que o engine seja grátis. É que o custo de manter um engine bem feito é menor que o custo de manter centenas de classes idênticas, quando o domínio é leitura uniforme. Fora desse contexto, a equação muda.
E Quando o Endpoint Não É Leitura Pura?
Tudo que foi dito até aqui se aplica a um perfil específico: endpoints que leem dados, aplicam filtros e devolvem JSON. O pipeline é previsível, as variações são cosméticas, e a lógica real mora na query.
Endpoints de escrita são outra história. Um POST /pedidos que cria um pedido pode precisar:
- Validar regras de negócio que dependem do estado atual (estoque disponível, limite de crédito, horário de operação)
- Disparar side effects (notificações, eventos de domínio, integrações com sistemas externos)
- Orquestrar múltiplas operações em transação
- Aplicar políticas de autorização granulares (quem pode criar pedido para qual filial?)
Essas variações são reais. O comportamento entre um POST /pedidos e um POST /devoluções é fundamentalmente diferente. Não é só "troca a query". Aqui, classes separadas fazem sentido. Interfaces com implementações distintas fazem sentido. O polimorfismo está polimorfizando de verdade.
O erro não é usar classes e interfaces. O erro é usar classes e interfaces onde não há variação de comportamento. A pergunta que separa os dois casos é simples: "se eu trocar a implementação dessa classe por outra, o corpo do método muda ou só muda qual dado ela busca?". Se só muda o dado, é configuração. Se muda o comportamento, é código.
Por Que Ninguém Percebe (E Por Que Não É Culpa de Ninguém)
Seria fácil apontar o dedo e dizer "o time não pensou direito". Mas a realidade é mais honesta que isso.
1. O padrão se instala gradualmente, e cada passo faz sentido isoladamente. Ninguém senta e decide "vou escrever 383 ifs". O primeiro endpoint é criado com cuidado. O segundo copia o primeiro. Faz sentido, é consistente. O terceiro copia o segundo. Faz sentido, segue o padrão. No centésimo, ninguém questiona porque questionar significaria reescrever noventa e nove endpoints que já funcionam. O absurdo se normaliza por repetição, e cada repetição individual é uma decisão local razoável.
2. Pressão de entrega não deixa espaço para revisão estrutural. Quando o backlog tem 40 endpoints para entregar no quarter, ninguém vai parar para perguntar "será que a gente deveria repensar como endpoints são criados?". A resposta racional no curto prazo é sempre "copia o padrão existente e entrega". Revisão arquitetural é um investimento que compete com features. E features têm dono cobrando. Arquitetura raramente tem.
3. As boas práticas mascaram o problema. "Cada classe tem responsabilidade única." Tem. A responsabilidade de ser um if específico. "Usamos interfaces para desacoplamento." Desacoplamento de quê, se todas as implementações são idênticas? "Injeção de dependência facilita testes." Testes de quê, se não há lógica para testar? As práticas são boas em contextos onde há variação real de comportamento. Quando aplicadas mecanicamente a centenas de casos idênticos, viram cargo cult. Um cargo cult que passa em code review porque tem a forma certa.
4. A alternativa parece "menos profissional". Um arquivo YAML com 300 linhas parece simples demais. Parece que "não tem engenharia". Um projeto com 800 arquivos e arquitetura em camadas parece robusto. Parece que "foi bem pensado". Confundimos volume com valor. E essa confusão é reforçada pela indústria: frameworks, cursos e livros frequentemente ensinam a estrutura sem questionar se o problema exige aquela estrutura.
5. Não existe incentivo organizacional para simplificar. Ninguém é promovido por reduzir o número de arquivos no projeto. Ninguém ganha reconhecimento por dizer "esse endpoint não precisa de classe própria". O incentivo é entregar features, e o caminho de menor resistência para entregar features é copiar o que já existe. Mudar a estrutura é risco sem recompensa visível no curto prazo.
Reconhecer isso não é justificar. É entender que a solução não é só técnica. Mudar o padrão exige espaço organizacional para revisão, ownership sobre a arquitetura, e uma cultura onde simplificar é valorizado tanto quanto construir.
Considerações Finais
A próxima vez que você abrir um projeto e encontrar centenas de classes fazendo a mesma coisa com variações cosméticas, pergunte: "qual é o n % 2 desse problema?". Qual é a regra genérica que resolve todos os casos sem enumerar cada um?
Se a resposta é "uma tabela de configuração + um engine", e o domínio é leitura uniforme, então o projeto está pagando um custo desnecessário em horas de desenvolvimento, PRs impossíveis de revisar, onboarding artificialmente longo, e refactors que ninguém quer fazer.
Mas a resposta nem sempre é essa. Algumas cerimônias existem por boas razões. Tipagem forte pega erros antes de produção. Contratos explícitos facilitam integração entre times. Classes separadas fazem sentido quando o comportamento realmente varia. O problema não é cerimônia em si. É cerimônia onde não há variação que a justifique.
A distinção que importa é: esse código expressa comportamento diferente, ou expressa o mesmo comportamento com nomes diferentes? Se é o mesmo comportamento, é configuração disfarçada de código. E configuração disfarçada de código acumula custo sem entregar valor.
Ifs disfarçados de classes continuam sendo ifs. Mas nem todo if é desnecessário. Só os que enumeram casos quando existe um padrão.
Links indicativos:
- Don't Repeat Yourself — The Pragmatic Programmer
- Configuration vs Convention vs Code
- The Rule of Three
- Cargo Cult Programming
- Accelerate — The Science of Lean Software — sobre como pressões organizacionais moldam decisões técnicas