Arquitetura de Software em C# no Mundo Real: Capítulo 3 – A falsa promessa do “service pattern” genérico
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 3 – A falsa promessa do “service pattern” genérico
Depois que você percebe que o modelo anêmico está te limitando, a reação mais comum é criar um service.
A lógica precisa ir para algum lugar, certo?
Aí nasce o UserService. Depois o AccountService, o PaymentService, o FooHandler, o BarManager.
Cada um carregando validações, regras de negócio, orquestração — e, com sorte, alguns comentários explicando o caos.
No começo, tudo parece resolvido. A lógica está “centralizada”. Você acha que está sendo organizado.
Mas, na prática, você só mudou o problema de lugar.
Services genéricos: o nome bonito do god object disfarçado
Vamos direto ao ponto: a maioria desses services não tem coesão.
Eles fazem coisas demais e não representam nenhuma ação do domínio de forma clara.
public class AccountService
{
public void CreateAccount(...) { ... }
public void CloseAccount(...) { ... }
public void TransferFunds(...) { ... }
public void ChangePrimaryContact(...) { ... }
public void EnableNotifications(...) { ... }
}
Esse AccountService faz tudo. Ele parece saber demais sobre o mundo e vira um ponto único de acoplamento para qualquer funcionalidade relacionada a "account".
Mas qual é o real caso de uso que ele está representando?
Nenhum em específico.
Cheiros clássicos de um service genérico
Você sabe que está lidando com um service fora de controle quando:
- Ele tem mais de 300 linhas (ou muito mais…)
- Contém métodos que nem se conhecem (ex:
ExportToCsv()convivendo comApproveTransaction()) - Recebe um número obsceno de dependências no construtor
- O mesmo método valida, transforma DTOs, persiste no banco e dispara notificações
- Você evita alterar algo porque “pode quebrar outra parte que não sei direito”
O que você perde mantendo esse padrão
- Coesão: seu código não representa uma ação clara do domínio. Os nomes mentem.
- Testabilidade: é difícil isolar comportamentos porque o service virou um Frankenstein de responsabilidades.
- Evolução: adicionar novos fluxos exige mexer em lugares já carregados, com risco alto de regressão.
- Refatoração impossível: qualquer mudança simples se espalha por múltiplas dependências.
O que começa como uma tentativa de organização, vira um anti-pattern estrutural que atrasa todo o time.
A alternativa: serviços voltados para casos de uso
Em vez de criar um service genérico para "tudo que envolve Conta", pense em ações específicas.
Em vez de AccountService.CloseAccount, você pode ter:
public class CloseAccountHandler
{
public Task Handle(CloseAccountCommand command)
{
// regras de negócio específicas para fechamento
}
}
Isso tem nome, escopo e foco. Representa um caso de uso real.
Você pode testá-lo isoladamente.
E ele não tem ambições de ser o centro do universo.
É só mais uma peça de um domínio bem particionado.
Esse é o ponto de virada onde muitos times começam a adotar Command Handlers e CQRS — que vamos explorar com mais profundidade nos próximos capítulos.
“Mas se eu quebrar os services grandes, vão sobrar centenas de classes…”
Sim. E isso é bom.
Ter 200 classes pequenas, cada uma responsável por um único caso de uso,
é melhor do que ter 5 classes gigantes com mil responsabilidades embaralhadas.
Arquitetura saudável é modular.
E isso exige granularidade.
“Mas o domínio já está encapsulado, então qual o problema de ter um orquestrador gordo?”
O problema é que esses orquestradores viram campos minados.
Você nunca sabe ao certo o impacto de alterar um pedaço, porque tudo está interconectado.
Quando você separa cada caso de uso em uma classe dedicada, você isola a complexidade
e cria um caminho natural para aplicar pipelines, validações, logs, transações e eventos de forma clara.
Conclusão
Services genéricos podem parecer uma forma natural de agrupar lógica,
mas acabam virando buracos negros de responsabilidade.
A alternativa — casos de uso pequenos, bem nomeados e testáveis —
é o caminho para um sistema que respira e escala.
Próximo capítulo:
Mas onde está a lógica que os services genéricos deveriam conter?
No próximo capítulo, vamos olhar de perto para as entidades —
e por que elas precisam deixar de ser apenas objetos com get; set; para se tornarem guardiãs do seu próprio estado.