TL;DR logo abaixo, mas vale ler com calma se você lidera ou influencia decisões técnicas em .NET.
Design patterns no .NET viraram quase um dialeto interno da plataforma. Em ASP.NET Core, Entity Framework, bibliotecas de terceiros e até no código da sua empresa, você encontra os mesmos padrões se repetindo: factory, builder, decorator, strategy, repository, e por aí vai.
Este artigo é um guia prático para liderança técnica e devs experientes que querem entender a fundo como os design patterns do .NET aparecem no dia a dia – mais do que “aplicar o pattern X”, a ideia é reconhecer esses padrões no framework e tomar decisões melhores de arquitetura, revisão de código e onboarding em bases legadas.
Ao longo do texto, vamos focar em:
- Qual problema cada pattern resolve.
- Como ele aparece no .NET / ASP.NET Core.
- Quando faz sentido usar (ou não usar) no seu código.
- Trade-offs reais, não slogans de livro.
TL;DR
Se você quer uma visão rápida:
- O .NET e o ASP.NET Core já embutem a maior parte dos padrões clássicos do GoF: Factory, Builder, Singleton (via DI), Adapter, Decorator, Facade, Observer, Strategy, Command, Template Method, Repository e Unit of Work.
- Em vez de “forçar pattern” no seu código, o ganho vem de reconhecer os patterns que já estão lá, entender o problema que eles resolvem e usá-los como vocabulário comum no time.
- Para liderança técnica, isso impacta:
- Arquitetura: decidir onde usar DI, onde encapsular integrações, onde aplicar CQRS.
- Code review: criticar decisões com base em problemas reais (acoplamento, testabilidade, clareza) e não em “faltou usar pattern X”.
- Onboarding: explicar código legado dizendo “aqui é um Decorator de logging”, “aqui é um Adapter para o gateway de pagamento”, em vez de “essa classe gigante faz um monte de coisa”.
- Overengineering é real: usar patterns demais, cedo demais, tende a deixar o código mais difícil de entender que o problema original.
Se quiser se aprofundar, siga a partir da seção 1.
1. Por que design patterns importam tanto no .NET hoje

Num ecossistema grande como .NET, você raramente está lidando só com “o seu código”. Em qualquer aplicação moderada você tem:
- O runtime (CLR).
- O BCL (biblioteca de classes base).
- ASP.NET Core, ORMs, bibliotecas NuGet, SDKs de terceiros.
- E o código da sua empresa, com suas camadas, domínios e integrações.
Todos esses blocos foram pensados com padrões de projeto comuns, o que faz com que design patterns do .NET funcionem como um vocabulário compartilhado:
- “Esse middleware é um Decorator.”
- “Essa interface é uma Strategy de serialização.”
- “Esse
DbContextage como Unit of Work + Repository.”
Quando todo mundo conhece esse vocabulário, discussões de design ficam muito mais objetivas.
Usar pattern vs. reconhecer patterns no framework
Existe uma diferença grande entre:
- “Vou aplicar o pattern X aqui”
(e às vezes forçar um pattern onde umifresolvia).
e
- “Esse método do framework está implementando um Template Method”
(e você só precisa se plugar num gancho).
O foco deste artigo é o segundo: ler e projetar código .NET/C# reconhecendo os patterns que já existem, e só então decidir se vale reproduzir esse estilo no seu código.
Se você quiser reforçar fundamentos de boas práticas .NET em paralelo, um bom complemento é revisar boas práticas de código C# e .NET em materiais como este artigo sobre boas práticas em .NET e C#.
Como isso ajuda liderança técnica
Para quem é tech lead, arquiteto ou referência técnica, entender design patterns do .NET muda a forma de atuar em:
- Decisão de arquitetura
- Vale isolar esse gateway de pagamento com um Adapter?
- Esse cross-cutting (logging/retry/cache) fica melhor com Decorator ou middleware?
-
Headless CQRS ou um simples Command Handler resolve?
-
Code review mais objetivo
Em vez de “gostei/não gostei”, dá para discutir: - “Seu
UserServiceestá virando um God Object; talvez o papel aqui seja de uma Facade sobre repositórios + integrações.” -
“Você criou um Singleton manual com
static, mas já tem DI. Que tal deixar o container cuidar?” -
Onboarding em bases legadas
Explicar “aqui tem um Observer de evento de domínio; ali temos uma Strategy de cálculo de preço” é mais eficaz do que “tem uma mágica aqui que chama isso e aquilo”.
[TRADE-OFF] Overengineering: quando patterns ajudam vs. complicam
Patterns são ferramentas, não metas. Problemas típicos:
- Aplicar pattern cedo demais: “vai que um dia a gente precisa de mais 5 estratégias” → hoje você tem 1, e 7 interfaces para mantê-la.
- Patterns em cascata: cada camada usa 3 patterns diferentes, o fluxo vira um labirinto de factories, facades e decorators.
Regra prática:
Comece simples. Use patterns quando o problema aparecer com clareza (variedade de comportamentos, múltiplas integrações, responsabilidades cruzadas etc.). Antes disso, um código direto costuma ser melhor.
2. Padrões de criação em .NET: mais do que new
2.1 Factory Method e Abstract Factory no dia a dia do C
Patterns de criação respondem à pergunta: “como instanciar isso sem acoplar todo mundo a new e a detalhes de configuração?”.
ASP.NET Core Options: fábricas de configuração
O pattern Options em ASP.NET Core é um ótimo exemplo de “fábrica de objetos” orientada a configuração.
public class MyOptions
{
public string Endpoint { get; set; } = "";
public int TimeoutSeconds { get; set; } = 30;
}
// Program.cs / Startup.cs
builder.Services.Configure<MyOptions>(builder.Configuration.GetSection("MyService"));
// Em qualquer serviço:
public class MyService
{
private readonly HttpClient _client;
public MyService(HttpClient client, IOptions<MyOptions> options)
{
_client = client;
_client.BaseAddress = new Uri(options.Value.Endpoint);
_client.Timeout = TimeSpan.FromSeconds(options.Value.TimeoutSeconds);
}
}
IOptions<MyOptions> funciona como uma espécie de Factory Method configurável:
- Você não instancia
MyOptionsdiretamente. - O container sabe como construir a instância (a partir da configuração).
- Quando você muda o binding, não precisa reescrever quem consome
MyOptions.
Se você usa IOptionsMonitor<>, ganha uma “fábrica reativa”: sempre que a configuração muda, o “produto” mudará no próximo acesso.
DbProviderFactory e Abstract Factory
O ADO.NET traz um exemplo clássico de Abstract Factory com DbProviderFactory:
DbProviderFactory factory = DbProviderFactories.GetFactory("System.Data.SqlClient");
using var connection = factory.CreateConnection();
connection.ConnectionString = connectionString;
using var command = factory.CreateCommand();
command.Connection = connection;
command.CommandText = "SELECT * FROM Users";
Aqui, DbProviderFactory é uma fábrica abstrata que sabe criar uma família de objetos relacionados:
DbConnection,DbCommand,DbDataAdapteretc.- A implementação concreta (SQL Server, MySQL, etc.) fica escondida atrás da factory.
Quando criar suas próprias factories
Você raramente precisa de uma Abstract Factory “formal” na aplicação, mas vale usar o padrão quando:
- Tem bounded contexts que mudam o “tipo” das dependências:
- Ex.: na vertical “Pagamentos”, cada método (cartão, boleto, PIX) precisa de clientes e configurações diferentes.
- Precisa de plug-in de integrações:
- Gateways de pagamento, antifraude, serviços de e-mail/SMS de fornecedores diversos.
Um esqueleto simples:
public interface IPaymentGateway
{
Task<PaymentResult> ChargeAsync(PaymentRequest request);
}
public interface IPaymentGatewayFactory
{
IPaymentGateway Create(string method);
}
public class PaymentGatewayFactory : IPaymentGatewayFactory
{
private readonly IServiceProvider _provider;
public PaymentGatewayFactory(IServiceProvider provider)
{
_provider = provider;
}
public IPaymentGateway Create(string method)
{
return method switch
{
"credit-card" => _provider.GetRequiredService<CreditCardGateway>(),
"pix" => _provider.GetRequiredService<PixGateway>(),
_ => throw new NotSupportedException($"Payment method {method} not supported")
};
}
}
[PITFALL] Factories que viram God Objects
Perigo clássico:
- A factory começa apenas escolhendo que implementação usar.
- Aos poucos, começa a:
- Validar input.
- Fazer logging.
- Chamar integrações.
- Disparar eventos.
Quando você vê, a “factory” virou um service gigante, misturando criação de objetos com regras de negócio. Sinal de que você precisa separar responsabilidades:
- Uma classe bem pequena para escolher/criar (a factory mesmo).
- Outras para regras de negócio e orquestração.
2.2 Builder: lendo e escrevendo código fluente em C
A maior parte das APIs fluentes em C# são variações do pattern Builder: você vai compondo chamadas que acumulam estado até “concluir” a construção ou configuração de algo.
Exemplos: HttpClient, IServiceCollection, IApplicationBuilder
ASP.NET Core é Builder do começo ao fim:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddControllers()
.AddJsonOptions(o =>
{
o.JsonSerializerOptions.PropertyNamingPolicy = null;
});
builder.Services.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp");
});
var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Aqui:
WebApplicationBuilderé um Builder da aplicação.- O pipeline
app.UseXxx()é um Builder de middlewares. IServiceCollectionconstrói o grafo de dependências via uma API fluente.
Como liderança, isso é útil porque:
- APIs internas da sua empresa, usadas por vários squads, podem usar o mesmo estilo:
services.AddBusinessModuleX();builder.AddTenantSupport().AddMultiRegion();- Builder evita construtores gigantes com 10 parâmetros opcionais; você passa a ter uma API de configuração passo a passo.
[TRADE-OFF] Builder vs múltiplos construtores vs objetos de request
- Construtores múltiplos:
- Bons para tipos simples, com poucas variações.
-
Ficam ruins quando você tem muitas combinações de parâmetros opcionais.
-
Objeto de request:
- Funciona bem quando você tem um “comando” claro.
-
Pode virar um saco de propriedades sem expressão clara se você não cuidar.
-
Builder:
- Brilha quando:
- Há muitos parâmetros configuráveis.
- Você quer uma API expressiva que reflita o fluxo de configuração.
- Custo:
- Mais tipos e código de “cola”.
- Pode esconder dependências implícitas (por exemplo, ordem de chamadas importa).
Regra pragmática:
Se o consumo do seu tipo está ficando confuso (“qual construtor eu uso mesmo?”), considere um Builder ou uma API fluente.
2.3 Singleton bem-feito (e mal-feito) no .NET moderno
No .NET moderno, você raramente deveria implementar o Singleton clássico com static. Você quase sempre quer deixar o container de DI cuidar do ciclo de vida.
Serviço singleton no ASP.NET Core vs pattern clássico
builder.Services.AddSingleton<ISystemClock, SystemClock>();
Aqui:
ISystemClockterá uma única instância por container (geralmente, por aplicação).- Quem precisa dela pede no construtor; você não precisa de
GetInstance()estático.
Isso resolve:
- Concorrência (thread-safe, controlada pelo container).
- Testes (você troca a implementação na configuração de DI).
- Ordem de inicialização (o container controla).
[PITFALL] Fazer Singleton manual com static
public class ConfigManager
{
private static readonly ConfigManager _instance = new();
public static ConfigManager Instance => _instance;
private ConfigManager() { /* carrega config */ }
public string GetValue(string key) { ... }
}
Problemas:
- Dificulta testes (não consegue trocar o
ConfigManager). - Embute dependências globais (acoplamento oculto).
- Pode criar problemas de ordem de inicialização se houver
staticcomplexos.
Na maioria dos casos, prefira:
public interface IConfigManager
{
string GetValue(string key);
}
public class ConfigManager : IConfigManager
{
// ...
}
// Program.cs
builder.Services.AddSingleton<IConfigManager, ConfigManager>();
Uso aceitável de singletons
Bom uso de lifetime singleton via DI:
- Configurações imutáveis: tipos de opções carregados uma vez.
- Clientes compartilhados:
HttpClientviaIHttpClientFactory.- Clientes de filas, caches e outros recursos caros.
3. Padrões estruturais que você encontra em todo código .NET

3.1 Adapter: tornando APIs “compatíveis” em integrações
Adapter é o padrão de “conversar com o mundo externo sem sujar seu domínio”. Você expõe uma interface que faz sentido pra você, e adapta o SDK externo para essa interface.
Exemplo no BCL: Stream e wrappers
Stream é uma abstração. Implementações como CryptoStream e GZipStream adaptam um stream base para adicionar comportamento:
using var file = File.OpenRead("data.txt");
using var gzip = new GZipStream(file, CompressionMode.Decompress);
using var reader = new StreamReader(gzip);
string content = reader.ReadToEnd();
GZipStreamadapta umStreampara lidar com compressão.- A API pública ainda é “um
Stream”.
Integrações típicas: SDKs de fornecedores
Com DDD / Ports & Adapters, você:
- Define uma porta (interface) alinhada ao domínio.
- Cria Adapters para cada fornecedor.
public interface IPaymentProvider
{
Task<PaymentResult> ChargeAsync(PaymentRequest request);
}
public class AcmePaymentAdapter : IPaymentProvider
{
private readonly AcmeSdkClient _client;
public AcmePaymentAdapter(AcmeSdkClient client)
{
_client = client;
}
public async Task<PaymentResult> ChargeAsync(PaymentRequest request)
{
var acmeRequest = new AcmeChargeRequest
{
Amount = request.Amount,
CardToken = request.CardToken
};
var response = await _client.ChargeAsync(acmeRequest);
return new PaymentResult
{
Success = response.Status == "OK",
TransactionId = response.Id
};
}
}
[PITFALL] Levar tipos de fornecedor para dentro do domínio
Erro comum:
- Models do SDK (
AcmeChargeRequest,AcmeChargeResponse) aparecem no domínio, nas entidades, nos serviços de negócio.
Consequências:
- Domínio acoplado a um fornecedor específico.
- Migrar de fornecedor vira cirurgia de grande porte.
Regra: SDK externo não entra no domínio. Use Adapter como zona de contenção.
3.2 Decorator: middleware, pipelines e cross-cutting concerns

Decorator é o padrão de “enrolar” um serviço com outro para adicionar comportamento.
ASP.NET Core Middleware como Decorator em cadeia
O pipeline app.UseXxx() é literalmente uma cadeia de Decorators:
app.Use(async (context, next) =>
{
// Antes
await next();
// Depois
});
Cada middleware:
- Recebe um
RequestDelegate(next). - Faz algo antes ou depois da chamada ao próximo.
Isso é Decorator puro: um objeto que implementa a mesma “assinatura” e adiciona comportamento em volta do outro.
DelegatingHandler no HttpClient
Outro exemplo direto:
public class LoggingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken ct)
{
Console.WriteLine($"Request: {request.Method} {request.RequestUri}");
var response = await base.SendAsync(request, ct);
Console.WriteLine($"Response: {response.StatusCode}");
return response;
}
}
Você pode encadear vários DelegatingHandler via IHttpClientFactory, formando um pipeline de Decorators.
[EXEMPLO] Decorar um serviço de domínio com logging
public interface IOrderService
{
Task<Order> GetByIdAsync(Guid id);
}
public class OrderService : IOrderService
{
public Task<Order> GetByIdAsync(Guid id)
{
// Busca no repositório, regra de negócio etc.
}
}
public class LoggingOrderServiceDecorator : IOrderService
{
private readonly IOrderService _inner;
private readonly ILogger<LoggingOrderServiceDecorator> _logger;
public LoggingOrderServiceDecorator(
IOrderService inner,
ILogger<LoggingOrderServiceDecorator> logger)
{
_inner = inner;
_logger = logger;
}
public async Task<Order> GetByIdAsync(Guid id)
{
_logger.LogInformation("Fetching order {OrderId}", id);
var order = await _inner.GetByIdAsync(id);
_logger.LogInformation("Fetched order {OrderId}", id);
return order;
}
}
Em containers que suportam Decorator, você registra algo como:
builder.Services.AddScoped<IOrderService, OrderService>();
// builder.Services.Decorate<IOrderService, LoggingOrderServiceDecorator>();
[TRADE-OFF] Decorator vs AOP vs filtros
- Decorator:
- Funciona bem quando você controla DI e contratos.
-
Transparente e explícito (vejo os decoradores na composição).
-
AOP (Aspect-Oriented Programming):
- Pode ser poderoso para cross-cutting (log, audit, retry).
-
Frequentemente é “mágico”, difícil de rastrear no debug.
-
Filtros MVC / filtros de pipeline:
- Bons para preocupações ligadas a HTTP (autorização, validação, response formatting).
- Menos adequados para lógica puramente de domínio.
Regra pragmática:
Se o cross-cutting é sobre requisições HTTP, use middleware/filtros.
Se é sobre serviços de domínio, prefira Decorator.
3.3 Facade: estabilizando fronteiras entre contextos
Facade é o padrão de “porta de entrada simples para um subsistema complexo”.
“Services” e “Managers” como Facade
Nem todo UserService é um anti-pattern. Alguns realmente:
- Orquestram vários repositórios.
- Chamam integrações.
- Coordenam transações.
Exemplo:
public class CheckoutFacade
{
private readonly ICartRepository _cartRepository;
private readonly IPaymentProvider _paymentProvider;
private readonly IOrderRepository _orderRepository;
public CheckoutFacade(
ICartRepository cartRepository,
IPaymentProvider paymentProvider,
IOrderRepository orderRepository)
{
_cartRepository = cartRepository;
_paymentProvider = paymentProvider;
_orderRepository = orderRepository;
}
public async Task<CheckoutResult> CheckoutAsync(Guid cartId)
{
var cart = await _cartRepository.GetByIdAsync(cartId);
var paymentResult = await _paymentProvider.ChargeAsync(new PaymentRequest
{
Amount = cart.Total
});
if (!paymentResult.Success)
return CheckoutResult.Failed("Payment failed");
var order = Order.CreateFromCart(cart, paymentResult.TransactionId);
await _orderRepository.SaveAsync(order);
return CheckoutResult.Success(order.Id);
}
}
A aplicação web conversa só com a Facade, sem precisar conhecer todos os detalhes internos.
System.IO.File como Facade
System.IO.File encapsula várias APIs de IO:
var text = File.ReadAllText("file.txt");
File.WriteAllText("file.txt", "content");
Ele é uma Facade sobre:
- Streams.
- Buffers.
- Acesso ao sistema de arquivos.
[PITFALL] Service gigante que não é Facade, é falta de design
Sinais de alerta:
UserServicecom milhares de linhas.- Métodos sem relação clara:
CreateUser,ExportToCsv,SendWelcomeEmail,RebuildIndexes… - Mistura de:
- Regras de negócio.
- Infraestrutura.
- Detalhes de UI.
Nesses casos:
- Quebre em:
- Facades menores por caso de uso (ex.:
UserRegistrationFacade). - Serviços de domínio focados.
- Adapters para integrações.
4. Padrões comportamentais em .NET: eventos, comandos e pipelines
4.1 Observer e o modelo de eventos do .NET
Observer é o padrão de “alguém muda, vários observadores são notificados”.
Eventos (event, EventHandler)
public class Stock
{
private int _quantity;
public event EventHandler<int>? QuantityChanged;
public void Change(int value)
{
_quantity += value;
QuantityChanged?.Invoke(this, _quantity);
}
}
Quem se interessa pelo evento se registra:
var stock = new Stock();
stock.QuantityChanged += (sender, newQuantity) =>
{
Console.WriteLine($"New quantity: {newQuantity}");
};
Isso é Observer clássico: Stock é o Subject, listeners são os Observers.
INotifyPropertyChanged em WPF/MAUI
Em UIs .NET, INotifyPropertyChanged é uma implementação padronizada de Observer:
public class PersonViewModel : INotifyPropertyChanged
{
private string _name = "";
public string Name
{
get => _name;
set
{
if (_name == value) return;
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
A UI observa o ViewModel e atualiza a tela quando as propriedades mudam.
[PITFALL] Leaks de memória com eventos
Em eventos baseados em delegates:
- Se o “observer” se inscreve num objeto de longa vida e não se desinscreve, o GC não coleta o observer, gerando vazamento.
Regra: em objetos de longa vida (singletons, serviços globais), sempre planeje o unsubscribe.
Eventos vs mensagens assíncronas
- Eventos .NET (Observer):
- Em memória, síncronos por padrão.
-
Bons para coordenação local dentro do processo.
-
Mensagens assíncronas (fila, bus):
- Cross-process, resilientes a falhas.
- Bons para integrações entre serviços, workflows longos.
Trade-off:
Se a reação ao evento é local e rápida, use Observer.
Se é entre serviços ou precisa sobreviver à queda de processos, use mensageria.
4.2 Strategy: configurando comportamento via DI

Strategy é o padrão de “trocar o algoritmo por composição”.
Interfaces e políticas injetadas
ASP.NET Core usa Strategy em vários pontos:
IPasswordHasher<>em Identity.IOutputFormatterem MVC.- Formas de cache, criptografia, serialização etc.
Exemplo simplificado:
public interface IPriceStrategy
{
decimal Calculate(decimal basePrice);
}
public class DefaultPriceStrategy : IPriceStrategy
{
public decimal Calculate(decimal basePrice) => basePrice;
}
public class BlackFridayPriceStrategy : IPriceStrategy
{
public decimal Calculate(decimal basePrice) => basePrice * 0.7m;
}
public class PricingService
{
private readonly IPriceStrategy _strategy;
public PricingService(IPriceStrategy strategy)
{
_strategy = strategy;
}
public decimal GetFinalPrice(decimal basePrice)
=> _strategy.Calculate(basePrice);
}
Registro:
builder.Services.AddScoped<IPriceStrategy, DefaultPriceStrategy>();
// em outro ambiente/cenário, trocar pela BlackFridayPriceStrategy
Trocando comportamento por configuração
Você pode decidir a Strategy via:
- Feature flags.
- Region/tenant.
- Tipo de cliente.
public class TenantPriceStrategySelector : IPriceStrategy
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IPriceStrategy _default;
private readonly IPriceStrategy _premium;
public TenantPriceStrategySelector(
IHttpContextAccessor httpContextAccessor,
DefaultPriceStrategy @default,
PremiumPriceStrategy premium)
{
_httpContextAccessor = httpContextAccessor;
_default = @default;
_premium = premium;
}
public decimal Calculate(decimal basePrice)
{
var tenant = _httpContextAccessor.HttpContext?.Request.Headers["X-Tenant"];
return tenant == "premium"
? _premium.Calculate(basePrice)
: _default.Calculate(basePrice);
}
}
[TRADE-OFF] Strategy vs if-else extensos
- Poucas variações + pouco acoplamento: um
switchdireto resolve, é mais simples. - Quando as regras crescem, se espalham pelo código e cada nova variação exige mexer em muitos
if/else, é hora de Strategy.
[PITFALL] Strategy demais
Sinal de overdesign:
- Você cria Strategy para tudo:
ILoggingStrategy,ISaveStrategy,IEmailStrategy…- O fluxo de negócio fica fragmentado em dezenas de classes.
Use Strategy onde há variação real de comportamento que você quer trocar sem reescrever o cliente.
4.3 Command, CQRS e pipelines em .NET
Command é o padrão de representar uma intenção como um objeto.
Commands como objetos de intenção
public record CreateOrderCommand(Guid CustomerId, IReadOnlyList<Guid> Items);
public interface ICommandHandler<TCommand>
{
Task HandleAsync(TCommand command, CancellationToken cancellationToken = default);
}
public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand>
{
public Task HandleAsync(CreateOrderCommand command, CancellationToken cancellationToken = default)
{
// criar pedido, gravar em repositório etc.
}
}
Você ganha:
- Separação clara entre intenção (“criar pedido”) e execução.
- Ponto único para validar, autorizar, processar.
Padrão Command em workers e filas
Você encontra Command em:
- APIs de UI (botões disparando comandos).
- Filas de trabalho: cada job é um “comando” a ser executado por um worker.
public class ProcessOrdersBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var command = await DequeueAsync(stoppingToken);
await HandleCommandAsync(command, stoppingToken);
}
}
private Task<CreateOrderCommand> DequeueAsync(CancellationToken ct)
{
// lê da fila, desserializa, etc.
}
private Task HandleCommandAsync(CreateOrderCommand command, CancellationToken ct)
{
// delega para ICommandHandler<CreateOrderCommand>
}
}
Cada mensagem da fila é um Command.
CQRS em cenários de alta complexidade
CQRS (Command and Query Responsibility Segregation) é uma evolução natural:
- Commands: mutam estado, não retornam dados complexos.
- Queries: só leem, sem efeitos colaterais.
Você pode implementar de forma leve:
public interface IQuery<TResult> { }
public interface IQueryHandler<TQuery, TResult>
where TQuery : IQuery<TResult>
{
Task<TResult> HandleAsync(TQuery query, CancellationToken cancellationToken = default);
}
[TRADE-OFF] CQRS completo vs “CQRS de bom senso”
- CQRS completo (com event sourcing, read models separados etc.) faz sentido quando:
- Você tem alta complexidade de leitura/escrita.
-
Há requisitos fortes de auditoria, histórico, reconstrução de estado.
-
CQRS de bom senso:
- Separar handlers de comando e consulta.
- Evitar métodos que fazem tudo (leitura + escrita + side effects) ao mesmo tempo.
Regra:
Comece com CQRS leve (comandos/queries separados).
Só adote event sourcing e modelos separados quando o problema justificar, não porque “está no livro”.
5. Patterns específicos do ecossistema .NET (implícitos, mas reais)
5.1 Dependency Injection como pattern de primeira classe
DI, como pattern, é central no ASP.NET Core.
DI container e IServiceProvider como Service Locator “escondido”
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IUserService, UserService>();
var app = builder.Build();
- O Composition Root é o
Program.cs: ali você define o grafo de objetos. - O framework injeta o que você precisa nos construtores dos controllers, services, handlers.
IServiceProvider é, tecnicamente, um Service Locator. Mas:
- Ele é confinado ao runtime.
- Você, na maioria dos casos, não precisa chamá-lo diretamente.
[PITFALL] Espalhar IServiceProvider.GetService pelo código
public class SomeClass
{
private readonly IServiceProvider _provider;
public SomeClass(IServiceProvider provider)
{
_provider = provider;
}
public void DoSomething()
{
var repo = _provider.GetService<IUserRepository>();
// ...
}
}
Isso vira:
- Service Locator explícito.
- Dependências ocultas (ninguém sabe que
SomeClassprecisa deIUserRepository). - Testes mais difíceis.
Regra: prefira injeção via construtor; use IServiceProvider direto só em pontos muito específicos (factories, integração com APIs legadas etc.).
[TRADE-OFF] DI minimalista vs frameworks de DI cheios de features
- DI minimalista do ASP.NET Core:
- Cobre a maioria dos casos sem complicar.
-
Não tem todos os recursos (auto-decorators, child containers avançados).
-
Frameworks de DI mais complexos:
- Podem dar features úteis (scan automático, decorators, interception).
- Adicionam complexidade de configuração e debug.
Como liderança, pense sempre em custo/benefício:
- O time entende as features extras?
- O ganho real compensa a curva de aprendizado e o custo de manutenção?
5.2 Template Method em frameworks baseados em herança
Template Method é o padrão de “classe base controla o fluxo, subclasses preenchem os detalhes”.
Controllers, handlers, frameworks UI
Você vê Template Method em:
- Controllers MVC:
- O framework controla o pipeline de request; você implementa ações e, às vezes, métodos como
OnActionExecuting,OnActionExecuted. - Handlers:
- Métodos
ExecuteAsync,HandleAsyncque são chamados pela infraestrutura. - UI (WPF, MAUI):
- Métodos
OnLoad,OnAppearing,OnInitialized.
Pseudocódigo:
public abstract class BackgroundJob
{
public async Task RunAsync()
{
await BeforeRunAsync();
await ExecuteAsync();
await AfterRunAsync();
}
protected virtual Task BeforeRunAsync() => Task.CompletedTask;
protected abstract Task ExecuteAsync();
protected virtual Task AfterRunAsync() => Task.CompletedTask;
}
O “template” (RunAsync) é fixo; subclasses só implementam os “ganchos”.
[TRADE-OFF] Template Method vs Strategy com composição
- Template Method:
- Simples de entender (herança, override).
-
Mas amarra a hierarquia: você só pode herdar de uma classe base.
-
Strategy + composição:
- Mais flexível (pode combinar estratégias diferentes).
- Mais tipos e indireções.
Como regra opinativa:
Se o framework já dita o fluxo e te dá ganchos, use Template Method sem culpa.
Se você está desenhando algo novo e quer compor comportamentos livremente, prefira Strategy via DI.
5.3 Repository, Unit of Work e o “pior e o melhor” do DDD em C
Repository e Unit of Work são onipresentes em .NET, principalmente via ORMs.
ORMs misturando Repository + Unit of Work
Um DbContext típico age como:
- Unit of Work:
- Rastreia mudanças.
-
Faz
SaveChanges()em lote. -
Repository genérico:
- Expondo coleções (
DbSet<T>) para CRUD.
public class OrdersController : ControllerBase
{
private readonly AppDbContext _db;
public OrdersController(AppDbContext db)
{
_db = db;
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateOrderDto dto)
{
var order = new Order(...);
_db.Orders.Add(order);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
}
}
Em cenários simples, isso é suficiente.
Quando faz sentido ter repositórios explícitos
Crie repositórios explícitos quando você quer:
- Proteger o domínio de detalhes de persistência.
- Expressar consultas de alto nível alinhadas ao negócio.
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id);
Task<IReadOnlyList<Order>> GetPendingOrdersAsync();
Task AddAsync(Order order);
Task SaveChangesAsync();
}
Dentro, você usa DbContext, mas o domínio conhece só a interface.
[PITFALL] Repositório-anêmico que só repassa chamada
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _db;
public Task<Order?> GetByIdAsync(Guid id)
=> _db.Orders.FindAsync(id).AsTask();
}
Se o repositório só delega chamadas, sem agregar nada:
- Você ganha quase nada além de código extra.
- A complexidade não é justificada.
Idealmente, repositórios adicionam:
- Consultas específicas.
- Semântica de domínio (“pedidos vencidos”, “pedidos pendentes”).
- Encapsulamento de detalhes de mapeamento.
Orientações opinativas para liderança
- Em contextos simples, usar diretamente o
DbContextnos handlers/controllers é aceitável e prático. - Em domínios complexos:
- Use repositórios explícitos para clareza e testabilidade.
- Deixe o
DbContextcomo detalhe de infraestrutura. - Evite DDD “cerimonial”: camadas a mais sem benefício claro só atrapalham o time.
6. Como liderar times para usar design patterns .NET com maturidade
Do “nome do pattern” ao “problema que ele resolve”
Patterns são atalhos de conversa, não medalhas. Em vez de:
- “Vamos usar Decorator aqui porque é legal.”
Prefira:
- “Temos múltiplos cross-cuttings nesse serviço (log, retry, cache). Decorator nos ajuda a compor isso de forma enxuta.”
Uma heurística útil é mapear:
- Problema → possíveis patterns.
Exemplos rápidos:
- Muitas variações de comportamento → Strategy.
- Cross-cutting em volta de um serviço → Decorator / middleware.
- Integração com fornecedor externo → Adapter + Facade.
- Complexidade de configuração/instanciação → Builder / Factory.
- Fluxo controlado pelo framework → Template Method.
Práticas para tech leads
Code reviews focados em clareza de intenção
Em código review, em vez de “faltou usar pattern X”, traga a conversa para o problema:
- “Hoje temos 5
if/elsediferentes para calcular preço. Que tal isolarmos isso em uma Strategy configurável?” - “Esse
UserServiceestá orquestrando subsistemas (Facade) ou simplesmente acumulando responsabilidades?”
Perguntas diretas ajudam:
- “O que está difícil de testar aqui?”
- “O que fica mais fácil de mudar se isolarmos em um pattern?”
Design sessions com comparação de alternativas
Antes de “fechar” em um pattern, coloque na mesa:
- Solução sem pattern dedicado (if/else, método simples).
- 1 ou 2 patterns candidatos.
- Impacto em:
- Complexidade de leitura.
- Testes.
- Extensibilidade real (não hipotética).
Isso puxa a discussão para “qual problema estamos resolvendo” em vez de “qual pattern é mais bonito”.
[TRADE-OFF] Padronizar demais vs permitir variação guiada
Guidelines fortes do tipo:
- “Sempre use Repository, mesmo para uma tela CRUD simples.”
- “Toda regra de negócio precisa estar em Strategy.”
Tendem a criar arquiteturas cerimoniais.
Melhor abordagem:
- Guidelines leves, com motivos claros:
- “Usamos Adapter para qualquer integração externa exposta ao domínio.”
- “Decorators são o padrão para cross-cuttings de domínio (log, retry, cache).”
- Deixe espaço para exceções bem justificadas.
Como treinar o olhar para reconhecer patterns
- Em código legado .NET:
- Peça para devs identificarem: “Onde há Strategy? Onde há Template Method? Onde há Singleton disfarçado?”
-
Use isso como ponto de partida para refactors, não como caça às bruxas.
-
Em frameworks e SDKs:
- Ao ler docs do ASP.NET Core, tente responder:
- “Que pattern está por trás desse middleware?”
- “Essa API de configuração é um Builder?”
Com o tempo, o time passa a falar em “Decorator”, “Adapter” e “Strategy” com significado concreto, não só como termos de livro.
Próximos Passos
Se você chegou até aqui, provavelmente já tem base para:
- Escolher 1–2 patterns para “ver” no código existente
-
Abra um projeto ASP.NET Core e marque:
- Onde há Decorator (middlewares, handlers,
DelegatingHandler). - Onde há Template Method (controllers, background services).
- Onde há Decorator (middlewares, handlers,
-
Selecionar 1 caso real do seu time para aplicar um pattern conscientemente
-
Ex.: extrair uma Strategy a partir de um método cheio de
ifs, ou criar um Adapter para isolar um SDK. -
Reforçar fundamentos de boas práticas .NET
-
Revise um material de referência como o artigo sobre boas práticas em .NET e C# e conecte as boas práticas aos patterns discutidos aqui.
-
Trazer o tema para o time
- Proponha uma sessão de leitura de código onde cada pessoa identifica patterns em uma parte da solução.
-
Discuta se cada pattern está ajudando ou só deixando o fluxo mais obscuro.
-
Se conectar com quem vive isso no dia a dia
- Participe da comunidade SCCB — https://instagram.com/software_craftsmanship
- Veja os próximos eventos — https://instagram.com/software_craftsmanship


