rcRacoelho
HomeBlogDesafiosProjetosSetupVagasComunidade
rcRacoelho

Conteúdo sobre desenvolvimento, tecnologia e desafios de programação para impulsionar sua carreira em tech.

Links rápidos
  • Blog
  • Desafios
  • Projetos
  • Setup
  • Vagas
  • Comunidade
  • Links
Conecte-se
    © 2024–2026 Racoelho. Todos os direitos reservados.v3.0.20 · build 2026-09-25
    rcRacoelho
    HomeBlogDesafiosProjetosSetupVagasComunidade
    blog / Design Patterns / design-patterns-que-todo-dev-deveria-conhecer
    Design Patterns

    Design Patterns que todo dev deveria conhecer

    Rafael Coelho
    26 fev 2026 · 12 min de leitura

    Se você acha que não usa design patterns, provavelmente está usando — só que sem saber.

    O ponto aqui não é “dar nome bonito” pra código, é ganhar repertório. Quando você reconhece um problema recorrente, você para de resolver no improviso e começa a escolher uma solução que já foi testada por muita gente em muito projeto.

    Aquele addEventListener no JavaScript é um Observer. Aquele switch que decide qual classe instanciar é um Factory. Aquele serviço que “só pode existir um” e você registra como singleton no container… adivinha.

    Design patterns não são enfeite acadêmico.

    São formas recorrentes de organizar código quando o sistema cresce e começa a exigir mudanças frequentes.

    E a parte mais importante: pattern não é “coisa pra colocar”. Pattern é coisa que aparece quando um problema existe de verdade.


    O que são design patterns de verdade?

    Em 1994 saiu o livro Design Patterns: Elements of Reusable Object-Oriented Software, do grupo que ficou conhecido como “Gang of Four”.

    A proposta era simples: documentar soluções reutilizáveis para problemas recorrentes em software orientado a objetos.

    Não era regra, framework, obrigação moral, nem “a forma correta” de programar. Era um catálogo. Um jeito de dizer: “quando você bater nesse tipo de problema, essas soluções costumam funcionar bem”.

    Eles listaram 23 padrões. Só que, no mundo real, você raramente precisa dos 23. Alguns poucos cobrem a maior parte das dores comuns em aplicações de verdade.

    Aqui vamos nos 7 que aparecem o tempo todo: Singleton, Builder, Factory, Facade, Adapter, Strategy e Observer.

    A estrutura do artigo vai ser sempre a mesma aqui: qual problema aparece, como você reconhece, qual solução o pattern propõe, como implementar e quais cuidados tomar.

    Use com sabedoria.


    1) Singleton — simples, útil e perigoso

    O problema que aparece

    Em certos cenários, você precisa garantir que exista uma única instância de um serviço durante toda a execução da aplicação. Isso acontece quando:

    • você tem estado compartilhado que precisa ser consistente (ex.: cache centralizado),

    • você tem recurso caro que não quer instanciar várias vezes (ex.: pool/gerenciador de conexões),

    • ou você quer ponto único de coordenação (ex.: logging, telemetry, config snapshot).

    Como reconhecer (sintomas)

    Você começa a ver coisas do tipo:

    • “se alguém instanciar isso duas vezes vai dar problema”

    • “isso precisa ser o mesmo em qualquer lugar do sistema”

    • “todo mundo precisa usar o mesmo objeto”

    A solução

    Singleton garante uma instância global acessível, controlando a criação do objeto.

    Implementação básica (C#)

    A versão clássica (lazy) seria:

    csharp
    public sealed class Logger { private static Logger? _instance; private static readonly object _lock = new(); private Logger() { } public static Logger Instance { get { if (_instance is not null) return _instance; lock (_lock) { _instance ??= new Logger(); return _instance; } } } public void Log(string message) { Console.WriteLine($"[{DateTime.Now:O}] {message}"); } }

    Versão mais segura e idiomática (Lazy<T>)

    Se você realmente for fazer Singleton manual, isso é melhor:

    csharp
    public sealed class Logger { private static readonly Lazy<Logger> _instance = new(() => new Logger()); private Logger() { } public static Logger Instance => _instance.Value; public void Log(string message) => Console.WriteLine($"[{DateTime.Now:O}] {message}"); }

    O “pulo do gato”: DI container já faz isso

    Na prática, em .NET moderno, o “Singleton que presta” é:

    services.AddSingleton<ILogger, Logger>();

    E pronto. Você ganha controle de ciclo de vida, testabilidade e substituição fácil.

    Trade-offs e armadilhas

    Singleton vira problema quando é usado como estado global mutável. Isso cria:

    • acoplamento escondido (qualquer lugar mexe),

    • testes difíceis (estado “vaza” entre testes),

    • dependências implícitas (você não vê no construtor).

    Quando usar / quando evitar

    Use quando houver necessidade real de instância única e preferencialmente via DI. Evite quando for só “comodidade” (“ah, é mais fácil acessar global”).


    2) Builder — quando o construtor começa a ficar grande demais

    O problema que aparece

    Você tem um objeto que precisa ser criado com muitos parâmetros, vários opcionais e combinações. Isso gera:

    • construtores gigantes,

    • ordem de parâmetros difícil de lembrar,

    • chamadas ilegíveis,

    • e bugs por “troquei o parâmetro sem querer”.

    Como reconhecer (sintomas)

    Você vê chamadas assim e sente dor física:

    csharp
    var pedido = new Pedido( "Rafael", "Rua X, 123", "cartão", "DESCONTO10", "sem cebola", items, DateTime.Now.AddDays(3), 15.90m);

    Ou então você começa a criar overload atrás de overload e a classe vira um mini caos.

    A solução

    Builder separa a construção da representação final e permite montar o objeto passo a passo, com nomes claros.

    Implementação prática

    Vamos supor esse Pedido:

    csharp
    public class Pedido { public string Cliente { get; } public string Endereco { get; } public string Pagamento { get; } public string? Cupom { get; } public string? Observacao { get; } public IReadOnlyList<Item> Itens { get; } public DateTime EntregaEm { get; } public decimal Frete { get; } internal Pedido( string cliente, string endereco, string pagamento, string? cupom, string? observacao, IReadOnlyList<Item> itens, DateTime entregaEm, decimal frete) { Cliente = cliente; Endereco = endereco; Pagamento = pagamento; Cupom = cupom; Observacao = observacao; Itens = itens; EntregaEm = entregaEm; Frete = frete; } }

    Agora o Builder:

    csharp
    public class PedidoBuilder { private string? _cliente; private string? _endereco; private string? _pagamento; private string? _cupom; private string? _observacao; private List<Item> _itens = new(); private DateTime? _entregaEm; private decimal _frete; public PedidoBuilder ParaCliente(string cliente) { _cliente = cliente; return this; } public PedidoBuilder ComEndereco(string endereco) { _endereco = endereco; return this; } public PedidoBuilder PagandoCom(string pagamento) { _pagamento = pagamento; return this; } public PedidoBuilder ComCupom(string cupom) { _cupom = cupom; return this; } public PedidoBuilder Observacao(string obs) { _observacao = obs; return this; } public PedidoBuilder ComItens(IEnumerable<Item> itens) { _itens = itens.ToList(); return this; } public PedidoBuilder EntregaEm(DateTime data) { _entregaEm = data; return this; } public PedidoBuilder ComFrete(decimal frete) { _frete = frete; return this; } public Pedido Build() { // validação forte: builder bom falha cedo if (string.IsNullOrWhiteSpace(_cliente)) throw new InvalidOperationException("Cliente é obrigatório"); if (string.IsNullOrWhiteSpace(_endereco)) throw new InvalidOperationException("Endereço é obrigatório"); if (string.IsNullOrWhiteSpace(_pagamento)) throw new InvalidOperationException("Pagamento é obrigatório"); if (_itens.Count == 0) throw new InvalidOperationException("Pedido precisa de itens"); if (_entregaEm is null) throw new InvalidOperationException("Data de entrega é obrigatória"); return new Pedido( _cliente!, _endereco!, _pagamento!, _cupom, _observacao, _itens, _entregaEm.Value, _frete); } }

    Uso:

    csharp
    var pedido = new PedidoBuilder() .ParaCliente("Rafael") .ComEndereco("Rua X, 123") .PagandoCom("cartão") .ComCupom("DESCONTO10") .Observacao("sem cebola") .ComItens(items) .EntregaEm(DateTime.Now.AddDays(3)) .ComFrete(15.90m) .Build();

    Trade-offs

    Builder adiciona classes e código extra. Vale a pena quando:

    • existem muitos campos opcionais,

    • validação de consistência é importante,

    • e legibilidade do “setup” do objeto importa.

    Se a classe tem 3 parâmetros e acabou, Builder é overkill.


    3) Factory — criando objetos sem espalhar lógica

    O problema que aparece

    Você precisa instanciar classes diferentes dependendo de um input (tipo, config, feature flag, ambiente, etc.). Se você espalha new + switch em todo lugar, vira caos.

    Como reconhecer (sintomas)

    Você vê:

    • switch(tipo) em 5 arquivos diferentes,

    • criação duplicada,

    • mudanças que exigem “caçar” todos os lugares.

    A solução

    Centralizar criação em uma “fábrica”. Quem usa pede o que quer; a fábrica decide como criar.

    Implementação simples

    csharp
    public interface INotificacao { void Enviar(string mensagem); } public class NotificacaoEmail : INotificacao { public void Enviar(string mensagem) => Console.WriteLine($"EMAIL: {mensagem}"); } public class NotificacaoSms : INotificacao { public void Enviar(string mensagem) => Console.WriteLine($"SMS: {mensagem}"); }

    Factory:

    csharp
    public static class NotificacaoFactory { public static INotificacao Criar(string tipo) => tipo switch { "email" => new NotificacaoEmail(), "sms" => new NotificacaoSms(), _ => throw new ArgumentException("Tipo inválido") }; }

    Factory + DI (mais real)

    Quando começa a escalar, o “new” dentro da factory também vira um problema (dependências). A versão mais madura usa DI:

    csharp
    public interface INotificacaoFactory { INotificacao Criar(string tipo); } public class NotificacaoFactory : INotificacaoFactory { private readonly IServiceProvider _sp; public NotificacaoFactory(IServiceProvider sp) { _sp = sp; } public INotificacao Criar(string tipo) => tipo switch { "email" => _sp.GetRequiredService<NotificacaoEmail>(), "sms" => _sp.GetRequiredService<NotificacaoSms>(), _ => throw new ArgumentException("Tipo inválido") }; }

    E no container:

    csharp
    services.AddTransient<NotificacaoEmail>(); services.AddTransient<NotificacaoSms>(); services.AddSingleton<INotificacaoFactory, NotificacaoFactory>();

    Trade-offs

    Factory resolve criação espalhada, mas pode virar “God Factory” se você jogar tudo num switch infinito. Quando isso começa, normalmente vale partir para:

    • mapeamento por dicionário (Dictionary<string, Func<INotificacao>>)

    • registro por convention

    • ou strategy resolver isso melhor dependendo do cenário.


    4) Facade — simplificando quem está de fora

    O problema que aparece

    Você tem um fluxo de negócio com muitos passos e vários serviços internos. Sem organização, quem chama vira um “orquestrador acidental” e passa a conhecer detalhes demais.

    Como reconhecer (sintomas)

    • Controllers chamando 6 serviços na mão

    • “colar” de lógica no endpoint

    • repetição do mesmo fluxo em vários lugares

    A solução

    Facade cria uma interface simples que encapsula a orquestração.

    Implementação

    Suponha serviços separados:

    • ValidadorCartao

    • CalculadoraImpostos

    • DescontoService

    • GatewayPagamento

    • NotaFiscalService

    • EmailService

    Facade:

    csharp
    public class PagamentoFacade { private readonly ValidadorCartao _validador; private readonly CalculadoraImpostos _impostos; private readonly DescontoService _desconto; private readonly GatewayPagamento _gateway; private readonly NotaFiscalService _notaFiscal; private readonly EmailService _email; public PagamentoFacade( ValidadorCartao validador, CalculadoraImpostos impostos, DescontoService desconto, GatewayPagamento gateway, NotaFiscalService notaFiscal, EmailService email) { _validador = validador; _impostos = impostos; _desconto = desconto; _gateway = gateway; _notaFiscal = notaFiscal; _email = email; } public ResultadoPagamento Processar(Pedido pedido) { _validador.ValidarCartao(pedido.Cartao); var total = _impostos.Calcular(pedido); total = _desconto.Aplicar(total, pedido.Cupom); var transacao = _gateway.Cobrar(pedido.Cartao, total); _notaFiscal.Gerar(pedido, transacao); _email.EnviarConfirmacao(pedido.Cliente); return new ResultadoPagamento(transacao.Id, total); } }

    Quem chama não precisa conhecer a bagunça:

    var resultado = _pagamentoFacade.Processar(pedido);

    Trade-offs

    Facade pode virar um “método mágico” enorme.

    O ideal é:

    • encapsular orquestração, mas

    • manter dependências explícitas,

    • e não esconder regras de negócio “críticas” sem testes.


    5) Adapter — quando duas APIs não conversam

    O problema que aparece

    Você tem uma interface interna (contrato do seu sistema) e uma dependência externa (biblioteca, API, SDK) com outro contrato.

    Se você espalhar a biblioteca pelo sistema inteiro, você casa com ela.

    Solução

    Criar um adapter que implementa seu contrato e traduz chamadas para a API externa.

    Implementação

    Seu contrato:

    csharp
    public interface ILogger { void Info(string msg); void Erro(string msg); }

    Serilog tem outro contrato. Adapter:

    csharp
    public class SerilogAdapter : ILogger { private readonly Serilog.ILogger _serilog; public SerilogAdapter(Serilog.ILogger serilog) { _serilog = serilog; } public void Info(string msg) => _serilog.Information(msg); public void Erro(string msg) => _serilog.Error(msg); }

    Trade-offs

    Adapter cria uma camada a mais, mas compra:

    • desacoplamento,

    • facilidade de troca,

    • testes mais simples (mocka sua interface, não a biblioteca).


    6) Strategy — comportamento como dependência (e o fim do switch infinito)

    O problema que aparece

    Você tem várias regras que mudam com o contexto: preço, frete, imposto, antifraude, roteamento, validações por tipo… e normalmente isso vira um switch gigantesco.

    Solução

    Extrair o comportamento para estratégias intercambiáveis (implementações de uma interface).

    Implementação

    csharp
    public interface IPrecoStrategy { decimal Calcular(Pedido pedido); } public class PrecoNormal : IPrecoStrategy { public decimal Calcular(Pedido pedido) => pedido.Itens.Sum(i => i.Preco); } public class PrecoBlackFriday : IPrecoStrategy { public decimal Calcular(Pedido pedido) => pedido.Itens.Sum(i => i.Preco) * 0.7m; }

    Resolver a estratégia:

    csharp
    public class PrecoService { private readonly IPrecoStrategy _strategy; public PrecoService(IPrecoStrategy strategy) { _strategy = strategy; } public decimal Calcular(Pedido pedido) => _strategy.Calcular(pedido); }

    Escolha de strategy pode vir de config, user tier, feature flag, etc.

    Trade-offs

    Strategy gera mais classes. Vale quando:

    • regra muda com frequência,

    • cresce em número de variações,

    • e você quer extensão sem mexer no que já funciona.


    7) Observer — eventos e reatividade sem acoplamento

    O problema que aparece

    Um evento dispara várias consequências. Ex.: “pedido criado” precisa:

    • enviar email,

    • baixar estoque,

    • notificar dashboard,

    • gerar nota fiscal,

    • disparar analytics…

    Se você fizer tudo dentro do PedidoService, ele vira um monstro acoplado.

    Solução

    Publicar um evento e permitir que handlers se inscrevam.

    Implementação simples (conceitual)

    Evento:

    public record PedidoCriadoEvent(Pedido Pedido);

    Publicação:

    _eventBus.Publish(new PedidoCriadoEvent(pedido));

    Handler:

    csharp
    public class EmailHandler : IHandler<PedidoCriadoEvent> { public void Handle(PedidoCriadoEvent e) { _email.Enviar(e.Pedido.Cliente, "Pedido confirmado!"); } }

    Trade-offs

    Observer facilita extensão, mas pode gerar:

    • “fluxo invisível” se você não tem observabilidade,

    • ordem de execução indefinida,

    • debug mais complexo.

    A solução aqui não é “não usar”, é usar com:

    • logging/tracing,

    • nomes claros,

    • eventos bem definidos,

    • e testes de integração onde importa.


    O recado final (o que separa dev bom de dev decorador)

    Depois que você aprende design patterns, dá vontade de aplicar em tudo. Só que pattern não é estética — é ferramenta. Se o problema não existe, o pattern vira peso.

    Um if pequeno não precisa virar Strategy. Um objeto simples não precisa de Builder. Nem toda criação precisa de Factory.

    O objetivo não é “usar patterns”. O objetivo é reduzir acoplamento, aumentar clareza e facilitar mudança quando seu sistema inevitavelmente crescer.

    Se você conseguir olhar para seu código e reconhecer esses problemas cedo, você para de apagar incêndio e começa a construir com intenção.

    Leia tambémContinue lendo

    4 min

    O que são Observers

    Design Patterns
    6 min

    Dá pra fazer fila sem RabbitMQ? | Protokol

    postgres
    9 min

    O difícil de WebSocket não é mandar mensagem

    rcRacoelho

    Conteúdo sobre desenvolvimento, tecnologia e desafios de programação para impulsionar sua carreira em tech.

    Links rápidos
    • Blog
    • Desafios
    • Projetos
    • Setup
    • Vagas
    • Comunidade
    • Links
    Conecte-se
      © 2024–2026 Racoelho. Todos os direitos reservados.v3.0.20 · build 2026-09-25