Design Patterns essenciais que todo dev deveria conhecer
Conheça os padrões de projeto mais usados — Factory, Strategy, Observer, Singleton, Decorator e mais — com exemplos modernos, trade-offs e dicas de quando aplicar (e quando evitar) cada um.

Padrões de projeto são soluções testadas para problemas que se repetem no design de software. Eles não são bibliotecas que você instala, e sim vocabulário e estrutura: formas comprovadas de organizar objetos e responsabilidades. Neste guia você vai conhecer os padrões mais úteis no dia a dia, com exemplos modernos, e — tão importante quanto — aprender quando não usá-los.
O que são design patterns
O conceito foi consolidado em 1994 pelo livro Design Patterns: Elements of Reusable Object-Oriented Software, escrito por Gamma, Helm, Johnson e Vlissides — os "Gang of Four" (GoF). Eles catalogaram 23 padrões observados em sistemas reais, agrupados em três famílias (Gamma et al., 1994):
A ideia central é dar um nome a soluções recorrentes. Quando você diz "isso é um Observer", o time inteiro entende a estrutura sem precisar ler cada linha. Padrões são, antes de tudo, comunicação.
A inspiração dos autores veio fora da computação: o arquiteto Christopher Alexander descreveu "padrões" como soluções recorrentes para problemas de projeto urbano, cada uma com um nome, um contexto e consequências. A GoF transportou essa ideia para o software. Por isso um padrão nunca é só um trecho de código — é um problema, uma solução genérica e um conjunto de consequências (trade-offs) atrelados.
Um aviso desde já: padrão não é meta. Aplicar padrões para "deixar profissional" é uma forma de over-engineering. Use-os quando o problema que eles resolvem realmente aparecer.
Strategy: trocando algoritmos em tempo de execução
O padrão Strategy encapsula algoritmos intercambiáveis por trás de uma interface comum, permitindo escolher o comportamento em tempo de execução. É o antídoto para aquela cadeia de if/else que decide como fazer algo.
Imagine cálculo de frete com várias transportadoras:
interface CalculadoraFrete {
calcular(pesoKg: number, distanciaKm: number): number;
}
class FreteEconomico implements CalculadoraFrete {
calcular(peso: number, distancia: number) {
return peso * 0.5 + distancia * 0.1;
}
}
class FreteExpresso implements CalculadoraFrete {
calcular(peso: number, distancia: number) {
return peso * 1.2 + distancia * 0.3 + 15;
}
}
class Pedido {
constructor(private frete: CalculadoraFrete) {}
custoEntrega(peso: number, distancia: number) {
return this.frete.calcular(peso, distancia);
}
}
// uso
const pedido = new Pedido(new FreteExpresso());Adicionar uma nova modalidade significa criar uma nova classe, sem tocar no Pedido. Isso é a expressão direta do Princípio Aberto/Fechado, um dos O que é SOLID? Os 5 princípios do design orientado a objetos: aberto para extensão, fechado para modificação.
Em linguagens com funções de primeira classe, o Strategy muitas vezes nem precisa de classes — uma função basta:
type Frete = (peso: number, distancia: number) => number;
const economico: Frete = (p, d) => p * 0.5 + d * 0.1;
const expresso: Frete = (p, d) => p * 1.2 + d * 0.3 + 15;
function custoEntrega(frete: Frete, peso: number, distancia: number) {
return frete(peso, distancia);
}Essa é uma lição importante: muitos padrões da GoF foram pensados para linguagens OO clássicas sem closures. Em linguagens modernas, a intenção permanece, mas a implementação fica mais leve.
Factory: centralizando a criação de objetos
O padrão Factory (Method ou Simple Factory) tira do código cliente a responsabilidade de saber qual classe concreta instanciar. Em vez de espalhar new por todo lado, você concentra a decisão em um único ponto.
type TipoPagamento = "cartao" | "pix" | "boleto";
interface Pagamento {
processar(valor: number): void;
}
class PagamentoCartao implements Pagamento {
processar(valor: number) { /* ... */ }
}
class PagamentoPix implements Pagamento {
processar(valor: number) { /* ... */ }
}
class PagamentoBoleto implements Pagamento {
processar(valor: number) { /* ... */ }
}
function criarPagamento(tipo: TipoPagamento): Pagamento {
switch (tipo) {
case "cartao": return new PagamentoCartao();
case "pix": return new PagamentoPix();
case "boleto": return new PagamentoBoleto();
}
}O benefício: se a forma de construir um pagamento mudar, há um único lugar para ajustar. O Factory frequentemente caminha de mãos dadas com a O que é injeção de dependência? Inversão de controle explicada — em vez de o objeto criar suas dependências, ele as recebe prontas, muitas vezes produzidas por uma factory ou por um container.
Vale distinguir variações que costumam ser confundidas:
Observer: reagindo a mudanças de estado
O padrão Observer define uma relação um-para-muitos: quando um objeto (o subject) muda de estado, todos os seus dependentes (observers) são notificados automaticamente. É a base de sistemas de eventos, da reatividade de interfaces e de filas de notificação.
class Assunto {
constructor() { this.observadores = []; }
inscrever(obs) { this.observadores.push(obs); }
desinscrever(obs){ this.observadores = this.observadores.filter(o => o !== obs); }
notificar(dado) { this.observadores.forEach(obs => obs.atualizar(dado)); }
}
class PainelEstoque {
atualizar(produto) {
console.log(`Estoque de ${produto.nome}: ${produto.quantidade}`);
}
}
const estoque = new Assunto();
estoque.inscrever(new PainelEstoque());
estoque.notificar({ nome: "Teclado", quantidade: 12 });Se você já usou addEventListener no navegador, callbacks de eventos no Node ou frameworks reativos, já consumiu o Observer. O padrão desacopla quem gera o evento de quem reage a ele — nenhum dos dois precisa conhecer a implementação do outro.
Duas armadilhas valem atenção. A primeira é o vazamento de memória por observadores não removidos: se um observer se inscreve mas nunca chama desinscrever, o subject o mantém vivo para sempre. Em interfaces, isso é causa frequente de vazamento — sempre limpe inscrições quando um componente é destruído. A segunda é a ordem de notificação: não confie na ordem em que observers são chamados, pois ela vira acoplamento implícito e frágil.
Singleton: uma instância única (com ressalvas)
O padrão Singleton garante que uma classe tenha apenas uma instância e fornece um ponto global de acesso a ela. É usado para coisas como pools de conexão, configuração global ou um logger compartilhado.
class Configuracao {
private static instancia: Configuracao;
private dados: Record<string, string> = {};
private constructor() {}
static obter(): Configuracao {
if (!Configuracao.instancia) {
Configuracao.instancia = new Configuracao();
}
return Configuracao.instancia;
}
set(chave: string, valor: string) { this.dados[chave] = valor; }
get(chave: string) { return this.dados[chave]; }
}
const config = Configuracao.obter();O Singleton é o padrão mais controverso da lista. Por introduzir estado global e acoplamento implícito, ele dificulta testes (não dá para substituir facilmente a instância por um mock) e pode esconder dependências. Em muitos casos, injetar uma única instância via injeção de dependência alcança o mesmo objetivo sem o estado global. Use Singleton com parcimônia.
Há ainda um detalhe técnico: em ambientes concorrentes, a checagem if (!instancia) seguida da criação não é atômica — dois threads podem criar duas instâncias. Linguagens diferentes resolvem isso de formas distintas (inicialização estática, double-checked locking, sync.Once em Go). A complexidade extra é mais um argumento para preferir uma instância única gerenciada pelo container de injeção de dependência, que cuida disso por você.
Decorator: adicionando comportamento sem herança
O padrão Decorator envolve um objeto em outro que adiciona comportamento, mantendo a mesma interface. É a alternativa elegante à explosão de subclasses para cada combinação de recursos.
interface Notificador {
enviar(msg: string): void;
}
class NotificadorEmail implements Notificador {
enviar(msg: string) { console.log(`E-mail: ${msg}`); }
}
class ComLog implements Notificador {
constructor(private inner: Notificador) {}
enviar(msg: string) {
console.log(`[log] enviando: ${msg}`);
this.inner.enviar(msg);
}
}
class ComRetry implements Notificador {
constructor(private inner: Notificador, private tentativas = 3) {}
enviar(msg: string) {
for (let i = 0; i < this.tentativas; i++) {
try { return this.inner.enviar(msg); } catch { /* tenta de novo */ }
}
}
}
// composição: log -> retry -> email
const notificador = new ComLog(new ComRetry(new NotificadorEmail()));Cada camada é independente e combinável. Esse é o mecanismo por trás de middlewares de servidores web e de streams encadeados. O ganho é claro: você adiciona log, retry, cache ou métricas sem tocar na classe original — Princípio Aberto/Fechado de novo.
Outros padrões que valem conhecer
Além dos centrais, alguns aparecem o tempo todo:
Esses padrões estruturais e comportamentais são peças recorrentes em arquiteturas mais ambiciosas, como discute a O que é Arquitetura Limpa (Clean Architecture)?, em que padrões ajudam a manter as regras de negócio independentes de detalhes como banco e framework (Martin, 2017).
Padrões e os princípios SOLID
Os padrões da GoF não nasceram em um vácuo: eles são, em grande parte, aplicações concretas de princípios de design mais amplos. Conectar uns aos outros aprofunda o entendimento dos dois.
Quem domina os princípios SOLID reconhece que muitos padrões são apenas o "nome de fábrica" de combinações que esses princípios já sugeriam. Isso explica por que padrões emergem naturalmente da refatoração: ao seguir bons princípios, você reinventa os padrões sem perceber.
Antipadrões: o lado escuro
Tão importante quanto reconhecer bons padrões é identificar antipadrões — soluções recorrentes que parecem boas mas pioram o sistema:
Curiosamente, o uso indevido de padrões legítimos é uma das fontes mais comuns de antipadrões. Um Factory desnecessário, um Singleton onipresente ou um mar de Decorators sem propósito são "padrões" que viraram antipadrões por falta de critério. O remédio é sempre o mesmo: o padrão precisa resolver uma dor real e presente.
Trade-offs: o que todo padrão cobra
Nenhum padrão é grátis. Antes de aplicar, pese o que você ganha contra o que paga:
A pergunta-chave nunca é "qual padrão usar?", e sim "esse problema realmente existe no meu sistema hoje?". Se a resposta for não, o padrão é dívida disfarçada de boa prática.
Quando NÃO usar design patterns
A maior armadilha dos padrões é aplicá-los por status, não por necessidade. Sintomas de abuso:
Padrões adicionam indireção, e indireção tem custo cognitivo. Eles compensam quando o problema que resolvem é real: variação que precisa ser intercambiável (Strategy), criação que precisa ser centralizada (Factory), eventos que precisam de desacoplamento (Observer). Quando o problema não existe, o padrão só adiciona complexidade — exatamente o oposto do que o O que é Clean Code? Guia completo de código limpo busca.
A boa notícia é que você não precisa decidir tudo de antemão. Muitos padrões surgem naturalmente como resultado de O que é refatoração e quando aplicar: você começa com um if/else simples e, quando as variações se multiplicam, refatora para Strategy. O padrão emerge da necessidade, em vez de ser imposto no primeiro dia.
Como estudar padrões de forma produtiva
Perguntas frequentes
Preciso decorar os 23 padrões da GoF? Não. Na prática, um punhado — Strategy, Factory, Observer, Decorator, Adapter, Facade e Repository — cobre a maioria das situações. Conheça os demais o suficiente para reconhecê-los quando aparecerem, mas não invista em memorização.
Design patterns ainda fazem sentido em linguagens funcionais? A intenção sim, a forma nem sempre. Um Strategy vira uma função passada como argumento; um Observer vira um stream de eventos. Os problemas continuam existindo; as soluções ficam mais leves quando a linguagem tem funções de primeira classe e imutabilidade.
Padrão é o mesmo que arquitetura? Não. Padrões de projeto operam no nível de classes e objetos. Arquitetura opera no nível de módulos, camadas e serviços. Eles se complementam: uma boa arquitetura frequentemente usa padrões nos seus pontos de extensão.
Como evito o over-engineering com padrões? Adote a regra "faça funcionar simples primeiro". Só introduza um padrão quando a terceira variação aparecer ou quando a dor que ele resolve for concreta e presente. Refatorar para um padrão depois é barato; remover um padrão prematuro é caro.
Conclusão
Design patterns são o vocabulário compartilhado da engenharia de software. Conhecer Strategy, Factory, Observer, Singleton, Decorator e companhia dá a você um repertório de soluções comprovadas e, mais do que isso, uma linguagem comum para conversar sobre design com o time. Quando alguém diz "vamos usar um Observer aqui", uma estrutura inteira fica subentendida.
O segredo, porém, não é colecionar padrões — é aplicá-los com critério, ciente dos trade-offs que cada um cobra. Eles brilham quando o problema que motivou sua criação aparece de verdade e atrapalham quando são impostos por hábito ou status. Aprenda os padrões, reconheça-os no código alheio e, sobretudo, deixe que eles emerjam da necessidade real do seu sistema. Padrão bem usado é aquele que torna o código mais claro, não mais complicado.