Executando verificação de segurança...
16

Arquitetura de Software em C# no Mundo Real: Capítulo 11 – Mitos e Más Interpretações na Arquitetura .NET

Essa série de artigos nasceu da frustração — e da prática.

Se você já tentou organizar um sistema .NET complexo, sabe que não é fácil: cada time tem uma visão diferente, os tutoriais se contradizem, e os “padrões” muitas vezes atrapalham mais do que ajudam. Entre um GenericRepository<T> e um PaymentServiceHandlerProcessorFactory, fica difícil saber o que realmente importa.

Aqui, a proposta é simples: falar sobre arquitetura real, com um olhar crítico, direto e sem medo de questionar modinhas. Vamos explorar estilos, padrões, anti-padrões e armadilhas comuns que se acumulam ao longo dos anos — e mostrar alternativas que funcionam fora do slide de apresentação.

Essa série é para desenvolvedores que já sujaram as mãos. Que já herdaram legado, quebraram produção, e tentaram explicar DDD em reunião de grooming.

Sem fórmulas mágicas. Sem purismo. Só código, contexto e experiência real.


Índice da Série

Capítulo 1 – Os caminhos que a arquitetura pode tomar

Um panorama direto e sem rodeios sobre Transaction Script, Modelo Anêmico, Modelo Rico e DDD.

Capítulo 2 – O modelo anêmico vai te trair (e como saber quando fugir dele)

Quando usar um modelo anêmico parece mais simples, mas te afunda em bugs e complexidade acidental.

Capítulo 3 – A falsa promessa do “service pattern” genérico

AccountService, UserManager, FooHandler: nomes que escondem baixa coesão e design acoplado.

Capítulo 4 – Entidades ricas: encapsulamento é mais que private set;

Como construir entidades com comportamento real e proteger sua integridade.

Capítulo 5 – A camada de aplicação não é um lixo eletrônico

Application Services devem orquestrar, não conter regras de negócio nem virar dumping ground de dependências.

Capítulo 6 – Use Cases, Command Handlers e MediatR: o que separa clareza de confusão

Quando aplicar CQRS traz benefícios reais — e quando só adiciona complexidade desnecessária.

Capítulo 7 – Repositórios que respeitam o domínio (e evitam vazamento de infra)

Um repositório não é só um wrapper de DbContext. É um contrato do domínio com a persistência.

Capítulo 8 – Domínio expressivo: Value Objects, Invariantes e Eventos de Domínio

Modelando regras com segurança e intenção. O domínio deve falar por si.

Capítulo 9 – Dividindo para conquistar: a importância dos Bounded Contexts

DDD real exige saber onde termina um modelo e começa outro. E como manter essa separação.

Capítulo 10 – Anti-patterns arquiteturais que todo dev .NET já viu (ou cometeu)

Um olhar honesto sobre os maiores pecados que já cometemos em nome da “arquitetura”.

Capítulo 11 – Mitos e Más Interpretações na Arquitetura .NET

Por que MVC não é arquitetura, repositório não é para toda entidade, padrão não é enfeite, e SOLID não é carta branca pra travar refatoração.

Capítulo 11 – Mitos e Más Interpretações na Arquitetura .NET

Ou: o que a gente aprende errado, repete sem pensar, e ensina pior ainda.

Se tem uma coisa que a internet ensinou pra gente nos últimos anos é que saber o nome das coisas não é o mesmo que saber usá-las.
E arquitetura de software virou esse mar de termos citados fora de contexto, padrões aplicados por osmose, e discussões técnicas que mais parecem briga de quem decora mais sigla.

Neste capítulo, vamos encarar de frente alguns vícios conceituais que se espalharam pelo mundo .NET — alguns herdados da era pré-Core, outros alimentados por treinamentos, cursos rasos e diagramas didáticos que nunca passaram por uma base de código real.


1. Achar que MVC é DAL + BLL + UI com nomes diferentes

Esse erro é mais comum do que parece — principalmente entre devs que migraram de aplicações em 3 camadas clássicas (DAL, BLL, UI) e foram parar num projeto ASP.NET MVC ou Web API.

A confusão é mais ou menos assim:

Controller → BLL  
Model     → DAL  
View      → UI

Só que isso não é MVC.

MVC é um padrão de separação de responsabilidade dentro da camada de apresentação.
Ele não dita nada sobre persistência, domínio ou infraestrutura.

Na verdade:

  • Assume que você já tem um modelo de domínio,
  • Supõe que o controller só vai orquestrar e preparar dados para a view,
  • E que a view é apenas um refletor dos dados que recebeu.

Essa visão distorcida cria sistemas onde:

  • Toda lógica fica nos controllers (às vezes com mais de 500 linhas),
  • O “Model” são só os EF Entities,
  • E o domínio? Não existe — só um grande vácuo entre o controller e o banco.

MVC não substitui arquitetura.
É apenas um padrão de UI, usado dentro de algo muito maior.


2. Criar um repositório pra cada entidade

Outro clássico:

“Temos 32 entidades, então criamos 32 repositórios.”

Isso não é DDD, nem Clean Architecture.
Isso é um ORM disfarçado de arquitetura.

Você só precisa de repositório onde existe um Aggregate Root, ou seja:

  • Uma entidade que possui controle de consistência sobre outras.
  • Um ponto de entrada para transações no domínio.

Você não precisa de EmailRepository, PhoneRepository, AddressRepository...

Você precisa de CustomerRepository se Customer encapsula tudo isso.

Regra prática:
Se a entidade não tem identidade própria e regras de consistência internas, não merece um repositório.


3. Estudar design patterns como se fosse faculdade teórica — ou fingir que eles não existem

Tem desenvolvedor que leu todos os livros do GoF, sabe explicar Strategy no papel, recita Factory, Observer, Decorator… mas nunca aplicou nenhum de verdade.

Na prática:

  • Escreve if/else encadeado onde um Strategy resolveria com elegância.
  • Ou pior: ignora a complexidade, porque “dá pra resolver rápido assim mesmo”.

Por outro lado, tem o dev que despreza padrões:

“Esses padrões aí só complicam.”
“Prefiro fazer do meu jeito, mais direto.”

Aí o que ele faz?

Cria uma tabela de outbox… mas chama de SyncMessageBuffer, TempQueue, SomethingManager.

O problema aqui é não usar o vocabulário comum da engenharia de software.

Padrões não são modinhas.
São atalhos cognitivos.
São acordos de comunicação entre desenvolvedores.

Você não precisa forçar um padrão onde não cabe.
Mas também não precisa reinventar o que já tem nome, forma e prática consolidada.


4. Usar SOLID como dogma sem contexto

Esse é clássico: alguém joga uma sigla na conversa como se fosse carta de autoridade.

“Isso aí tá quebrando o OCP.”
“Tá violando SRP.”
“Tem que ter interface, por causa do DIP.”

Só que:

  • O “OCP” citado é mal interpretado — você só estava refatorando uma classe gorda, não alterando regra de negócio.
  • O SRP virou “uma classe por responsabilidade”, mesmo que a responsabilidade tenha 5 linhas.
  • O DIP virou “injeção de dependência sem propósito”.

E pior: vira uma conversa onde ninguém mais discute o problema real — só defesas baseadas em siglas, ego e medo de parecer que não conhece o acrônimo da vez.

SOLID é um guia prático, não um código penal.
Aplicar os princípios exige contexto, propósito e bom senso — não repetição automática.


Conclusão

Esses erros não acontecem por falta de capacidade técnica.
Eles acontecem porque, no dia a dia, a teoria vira ruído — e o time só quer entregar a feature e fechar o ticket.

Mas é aí que mora o perigo:

  • Quando você cita SOLID como escudo, e não como guia.
  • Quando rejeita padrões de projeto por “estilo próprio”, e não por decisão técnica consciente.
  • Quando lê sobre arquitetura, mas não enxerga os padrões na prática.
  • Quando transforma boas práticas em dogmas, ignorando o contexto.

O resultado disso tudo é sempre o mesmo:
Código difícil de entender, difícil de evoluir, e impossível de explicar.

Você não precisa aplicar todos os padrões. Nem deve.
Mas também não pode ignorar o conhecimento coletivo da engenharia de software em nome do “meu jeito”.

Saber programar bem é mais que saber sintaxe.
É tomar decisões conscientes, com base em propósito — e não em siglas ou alergia a abstração.


Fim da jornada (por enquanto)

Essa série não é um manual de regras.
É um convite ao senso crítico.

Modelar software é um trabalho criativo, iterativo e profundamente humano.

Você vai errar. Eu erro até hoje.

Mas quanto mais seu domínio fala, mais o código respira.
E quando o código respira, o time evolui junto.

Nos próximos artigos, podemos explorar temas como:

  • Integrações entre contextos (com anticorruption layers)
  • Event Sourcing
  • Técnicas para refatorar legado sem quebrar tudo
  • Como evoluir arquitetura mesmo quando o time resiste

Se você chegou até aqui:
obrigado pela companhia.
Nos vemos na próxima refatoração. 👊🏼

Carregando publicação patrocinada...
7

Apenas um pensamento de um nerd lendo um tópico nerd de como deixar seu código mais nerd. Doidera como gostamos de achar o melhor jeito para tal coisa, como queremos ficar orgulhosos de nós mesmos ao montar um serviço de e-mail robusto, testado e com uma boa DX (Developer Experience). Talvez seja um pouco da nossa vaidade, programação pode ser algo chato como tentar refatorar um sistema legado cheio de Services, mas também criar algo - não do absoluto zero, mas algo novo. Doidera como em um sábado à tarde onde eu deveria estar fazendo outras coisas (que convenhamos, é menos importante do que aprender DDD) estou no TabNews lendo artigos.

Pensando aqui que daqui alguns anos teremos ADD (AI Driven Development) ou JAA (Just Ask AI), programação é uma doidera.

2

Boa demais essa reflexão... e te entendo totalmente.

Mas acho que não é vaidade, não. A gente busca um código melhor porque sabe o peso que ele vai ter amanhã. Um serviço de e-mail robusto, com boa DX, não é pra inflar ego — é pra evitar dor de cabeça quando tudo estiver pegando fogo e alguém precisar mexer ali de novo.

No fim, escrever um código bem pensado é buscar paz de espírito. É querer dormir tranquilo sabendo que o sistema vai se comportar. E sim, se isso envolve ler sobre DDD num sábado à tarde… que seja.

7

Excelente conteúdo, parabéns!
Vou salvar para revisitar sempre que for contruir algo novo.
Mas fiquei com dois pontos de duvida.
1 - Onde ficariam os logs?
2 - Lançar exceções no domain é uma boa pratica? atualmente eu estou utilizando a abordagem de criar um contexto de notificação no dominio, e nele fica contido tudo que seria por exemplo uma exception.

5

Valeu demais pelo feedback, e ótimo que você levantou esses dois pontos — eles realmente ficaram como brechas no artigo.

  • Sobre logs:
    Uma boa abordagem é usar Domain Events.
    Você levanta o evento dentro da entidade ou aggregate (PaymentApproved, CustomerCreated, etc.) e depois trata isso num listener desacoplado que pode fazer o log, mandar alerta, ou qualquer outro efeito colateral.

A vantagem é que o domínio continua limpo — sem saber que existe ILogger — e você ainda ganha observabilidade.
Tô considerando escrever um artigo só sobre Domain Events, e esse tópico com certeza entra lá.

  • Sobre exceções no domínio:
    Esse é outro papo bom.
    A minha visão: exceção é pra erro inesperado, não pra fluxo alternativo.
    Se um pagamento não pode ser aprovado por causa de uma regra de negócio, o ideal é retornar um Result ou algo semelhante.
    Isso deixa o código mais funcional, mais testável e evita dependência do try/catch como mecanismo de controle de fluxo.

Tem várias libs que ajudam nisso: OneOf, LanguageExt, FluentResults, entre outras. Mas mesmo sem lib, um Result simples já resolve bem.

Valeu pelo comentário e por puxar esses temas — são os próximos tópicos naturais da série.
Um abraço! 👊🏼

7

Excelente série de artigos!
Tocou em vários assuntos que tenho estudado nos últimos anos em uma sequência só conectando os mesmos. Só senti falta de algumas referências para que possamos nos aprofundar mais em cada um dos tópicos.
Ainda tenho dúvidas sobre a persistência das modificações feitas na entidade, me parece que fica difícil não misturar alguns conceitos se a entidade for uma instância direta do model usado em um EF da vida. Seria necessário que o repositório (no caso de um Aggregate Root) fizesse a adaptação entre a entidade e o model usado para persistência?

2
4
5

Esse seu conteúdo seria ótimo como video do Youtube.

Não encontrei nenhum link no seu perfil, então recomendo muito que você crie um canal!

Se já existir, me manda on link que quero me inscrever!

1
1

Valeu demais pelo incentivo!
Eu tenho sim um canal no YouTube: rcarubbi — tá meio parado, mas tá lá!
Uns anos atrás eu até tentei levar a ideia adiante com um projeto chamado Código Nerd. A proposta era fazer vídeo-aulas interagindo com um boneco de dinossauro, tentando explicar conceitos técnicos de um jeito mais leve e divertido. Mas acabou não indo pra frente por falta de tempo e investimento mesmo.

Obrigado pelo comentário e pela motivação

5

Escreve if/else encadeado onde um Strategy resolveria com elegância.

Não entendo esse necessidade de deixar o código elegante. Para mim, código tem que ser simples e manutenível. Se um júnior cabaço olha um código elegante e não sabe o que fazer, seu código não foi bem escrito.
Sei que o post não é sobre isso, mas quis trazer esse ponto. Concordando com sua conslusão, que se resume muito bem quando diz:

O resultado disso tudo é sempre o mesmo:
Código difícil de entender, difícil de evoluir, e impossível de explicar.

1

A questão não é sobre “deixar o código elegante” no sentido estético ou sofisticado. É sobre deixar ele preparado pra mudar sem virar um campo minado.

No caso do Strategy, a ideia é simples: quando surge um novo cenário, você adiciona uma nova classe. Não precisa tocar nas outras. Não precisa abrir aquele if/else gigante, nem arriscar quebrar o comportamento de casos que já funcionam.

Já com if/else, toda vez que aparece uma nova variação, você tem que entrar no mesmo bloco de código, mexer num switch que cresce, revisar dependências… e esse acoplamento vai só piorando.

E isso afeta diretamente a manutenção.
Inclusive pro júnior que você citou — porque ele vai entender muito melhor um código separado por responsabilidade clara, do que um monolito de if/else que tenta cobrir todos os cenários.

Código simples e manutenível não significa “tudo em um lugar só”.
Significa “fácil de entender, mudar e testar sem medo”.

Valeu demais por levantar o ponto e contribuir com a discussão. É esse tipo de troca que melhora o código e a cabeça da galera. 👊🏼

4

Dificilmente um desenvolvedor júnior em início de carreira vai entender ou saber quando aplicar padrões de projeto. A leitura de um simples if/else é, de fato, muito mais direta do que ter que navegar por várias classes em um projeto extenso.

Infelizmente, é comum ver desenvolvedores implementando padrões complexos sem necessidade, muitas vezes para inflar o próprio ego. Eles tentam prever o futuro da aplicação, ignorando princípios mais relevantes como YAGNI e KISS. Muitas implementações nascem com a justificativa de que "no futuro poderemos precisar disso", o que leva à adoção desnecessária de padrões.

Eu mesmo já vi situações extremas, como a criação de uma factory para instanciar um command, mesmo quando havia apenas um único tipo de retorno, ou o uso de strategy para resolver o que poderia ser um simples if. Isso acontece, em parte, porque muitos artigos e tutoriais abordam esses temas de forma superficial, com exemplos rasos. Um desenvolvedor iniciante, então, acaba acreditando que essas implementações são regras absolutas, carregando essa ideia para sua carreira.

A chave é entender que padrões de projeto são ferramentas para resolver problemas específicos, não um fim em si mesmos. A simplicidade e a funcionalidade devem sempre vir em primeiro lugar.

1

De fato. Não existe bala de prata, e tentar prever o futuro inteiro da aplicação quase sempre leva a overengineering.
Mas vale pensar também pela ótica dos testes: cada if novo que você adiciona duplica a quantidade de caminhos que precisam ser cobertos. A complexidade ciclomatica cresce rápido, e logo aquele método “simples” vira um Frankenstein cheio de exceções e casos especiais.
Quando você separa o comportamento em classes isoladas (via Strategy, por exemplo), cada cenário pode ser testado de forma independente, sem precisar montar todo o estado global do mundo pra testar um único fluxo.
No fim, o bom senso de quando aplicar o quê vem mesmo com a experiência. A gente erra forçando padrão onde não precisa, e também erra mantendo tudo junto por medo de complicar.
O ponto é: não dá pra nivelar por baixo.
Buscar simplicidade não pode virar desculpa pra empilhar if até ninguém mais querer mexer no código.

4
0
4

Que série massa!
Em meio a tantas discussões sobre complexidade vs simplicidade, qualidade vs agilidade, é bom ver pessoas que defendem um meio termo de forma tão clara e objetiva.
MVP não deve ser sinônimo de "o que importa é fazer funcionar". Se você não desenvolve com o mínimo de qualidade, ele não escala com segurança. E não foi com o plano futuro de escalar que ele foi projetado?! Afinal, supõe-se que houve um mínimo estudo de viabilidade pra inferir se o tal MVP resolve a dor de alguém... da mesma forma sistemas mais simples: se o objetivo é funcionar como esperado, deve-se ter o mínimo de preocupação com qualidade.
Aguardando os próximos capítulos!