Patterns

Escopo: transversal. Aplica-se a qualquer linguagem ou stack do projeto.

Patterns de design são soluções consolidadas para problemas recorrentes: vocabulário compartilhado entre engenheiros e heurísticas testadas em produção.

Conceitos fundamentais

ConceitoO que é
Caller (quem invoca a função)Código que chama uma função ou serviço e trata o resultado
ORM (Object-Relational Mapper, Mapeador Objeto-Relacional)Biblioteca que mapeia objetos do código para tabelas do banco de dados
OCP (Open/Closed Principle, Princípio Aberto/Fechado)Design aberto para extensão por novas implementações, fechado para modificação do código existente
CRUD (Create, Read, Update, Delete, Criar, Ler, Atualizar, Deletar)Conjunto das quatro operações básicas de persistência
CQS (Command-Query Separation, Separação de Comando e Consulta)Princípio de função: retorna valor OU produz efeito colateral, nunca os dois; ver principles.md
CQRS (Command Query Responsibility Segregation, Segregação de Responsabilidade de Comando e Consulta)Padrão arquitetural: modelos de escrita e leitura completamente separados
Command (Comando)Operação que altera estado; não retorna dado de negócio
Query (Consulta)Operação que lê e retorna dado; não altera estado
Projection (Projeção)Modelo de leitura desnormalizado, otimizado para consulta
SDD (Spec-Driven Development, Desenvolvimento Orientado a Especificações)Spec define contrato de entradas, saídas e comportamentos antes de qualquer implementação
LLM (Large Language Model, Modelo de Linguagem de Grande Escala)Modelo de IA treinado em texto que gera código, explica conceitos e auxilia no desenvolvimento
Handler (processador de evento ou requisição)Função ou objeto que recebe um evento ou requisição e decide como processar
Middleware (intermediário de requisição)Componente em um pipeline que intercepta a requisição, processa e repassa para o próximo elo

Referência rápida

PatternProblema que resolveSinal de uso
ResultFalhas invisíveis na assinaturaOperação de domínio que pode falhar
RepositoryAcoplamento entre domínio e storageSELECT no meio do código de negócio
FactoryCriação complexa espalhada nos callersConstrutor com lógica condicional
BuilderConstrutor com muitos parâmetros opcionaisnew Obj(null, null, true, false, ...)
SingletonInstância duplicada para estado globalPool de conexões, config, logger
Strategyif/switch crescendo por tipoComportamento que varia por contexto
ObserverProdutor acoplado a consumidoresReações a eventos em cascata
DecoratorComportamento transversal sem modificar baseLogging, cache, retry composáveis
AdapterInterfaces incompatíveis entre domínio e externoIntegração com API ou lib de terceiro
FacadeSubsistema com muitos pontos de entradaOrquestração de múltiplos serviços
ProxyAcesso ao objeto precisa de interceptaçãoCache, controle de acesso, lazy init
Chain of ResponsibilityMúltiplos handlers em sequênciaPipeline de middleware, validação em etapas
CommandOperação precisa ser enfileirada ou auditadaFila de tarefas, undo/redo
StateComportamento muda por estado internoEntidades com ciclo de vida (pedidos, contratos)
Template MethodAlgoritmo fixo com etapas variáveis por tipoRelatórios, importações com formatos diferentes
CQRSWrite model e read model divergemRelatórios complexos, alto volume de leitura
AI-DrivenAceleração de geração com revisão críticaCiclos rápidos com spec bem definida
SDDSpec antes de códigoDecisões de design sem custo de implementação

Especializados, com aplicabilidade mais restrita e sem seção dedicada:

PatternProblema que resolveSinal de uso
Abstract FactoryCriação acoplada a uma família de objetos concretosSistemas com múltiplos temas, providers ou conjuntos intercambiáveis
PrototypeCriar objeto do zero é caroClone com ajustes é mais eficiente que instanciar
CompositeObjetos individuais e composições tratados de forma diferenteEstruturas em árvore: menus, categorias, UI aninhada
MediatorObjetos se referenciam diretamente criando acoplamento cruzadoEvent bus, formulários com dependências entre campos
MementoPrecisa restaurar estado anterior sem violar encapsulamentoUndo/redo, snapshots de sessão
BridgeAbstração e implementação crescem juntas em subclassesAmbas precisam variar de forma independente
VisitorNovas operações em estruturas heterogêneas exigem modificar classesAST, exportadores multi-formato
FlyweightMuitas instâncias com estado repetido consomem memóriaVolumes altos de objetos similares: partículas, caracteres em editores
IteratorAcesso interno à coleção exposto ao callerEmbutido via for...of e generators; raramente implementado diretamente

Result Pattern

Operações que podem falhar têm dois caminhos: sucesso e falha. A forma mais comum de tratar isso é lançar exceções, mas exceções são invisíveis na assinatura da função. Quem chama não sabe, sem ler a implementação, que a função pode falhar e em quais condições.

O Result pattern torna os dois caminhos explícitos na assinatura:

Result<Order>
  .Success(order)
  .Failure("SKU not found")

O caller (quem invoca a função) é obrigado a tratar os dois casos. Sucesso e falha são valores: ambos aparecem na assinatura e exigem tratamento explícito. Isso elimina try/catch espalhados pelo código de negócio e centraliza o tratamento de erro onde faz sentido.

Quando usar: operações de domínio que podem falhar por regra de negócio (validação, não encontrado, estado inválido). Exceções de infraestrutura (falha de banco, timeout de rede) seguem o caminho normal de exceções.

Factory

Criação de objetos complexos tem lógica: validar parâmetros, aplicar defaults (valores padrão), montar dependências. Colocar essa lógica no construtor mistura responsabilidades. Espalhá-la nos callers cria duplicação.

Factory centraliza a lógica de criação em um único lugar. O caller pede um objeto sem saber como ele é montado.

UserFactory.create({ name, email, role })
  → valida email
  → aplica role default se ausente
  → retorna User

Quando usar: criação envolve validação, defaults ou lógica condicional que não pertence ao caller.

Repository

O código de negócio não deveria conhecer SQL (Structured Query Language, Linguagem de Consulta Estruturada), ORM (Object-Relational Mapper, Mapeador Objeto-Relacional) ou detalhes de storage. Repository encapsula o acesso a dados atrás de uma interface orientada a domínio.

UserRepository
  .findById(id)
  .findByEmail(email)
  .save(user)

O código de domínio fala em findByEmail, não em SELECT * FROM users WHERE email = ?. A camada de dados pode mudar (PostgreSQL → MongoDB, Dapper → EF) sem tocar o domínio.

Quando usar: acesso a banco em sistemas com lógica de domínio não trivial. Em CRUDs (Create, Read, Update, Delete, Criar, Ler, Atualizar, Deletar) simples sem lógica, pode ser overhead (custo extra de implementação).

Strategy

Comportamento que varia por contexto (calculadora de frete, formatador de relatório, provedor de pagamento) tende a virar um if/switch crescendo indefinidamente. Strategy resolve isso extraindo cada variação em sua própria implementação com interface comum.

ShippingStrategy
  ├── CorreiosStrategy.calculate(order)
  ├── FedExStrategy.calculate(order)
  └── PickupStrategy.calculate(order)

O caller recebe a strategy como dependência. Adicionar uma nova variação é adicionar uma nova implementação, sem tocar o código existente. Isso é o OCP (Open/Closed Principle, Princípio Aberto/Fechado): aberto para extensão, fechado para modificação.

Quando usar: comportamento que varia por tipo, contexto ou configuração e que tem chance real de crescer.

Observer

Um evento ocorre e múltiplas partes do sistema precisam reagir. Conectar produtor e consumidores diretamente cria acoplamento: cada novo consumidor exige modificar o produtor.

Observer inverte esse acoplamento. O produtor emite um evento sem saber quem vai ouvir. Os consumidores se registram para os eventos que lhes interessam.

OrderPlaced (evento)
EmailService.sendConfirmation()
InventoryService.reserve()
AnalyticsService.track()

Adicionar um novo consumidor não toca o produtor. Remover um consumidor também não. O produtor e os consumidores evoluem de forma independente.

Quando usar: reações a eventos onde o produtor e os consumidores precisam evoluir de forma independente. Evitar quando a ordem de execução dos handlers (funções que respondem ao evento) importa, pois Observer não garante ordem.

Builder

Objetos com muitos parâmetros opcionais criam construtores ilegíveis e chamadas confusas. Builder constrói o objeto passo a passo, nomeando cada etapa.

QueryBuilder
  .from("orders")
  .where("status", "pending")
  .orderBy("created_at", "desc")
  .limit(20)
  .build()

Cada método retorna o próprio builder, permitindo encadeamento. O build() no final valida e retorna o objeto montado. A intenção de cada parâmetro fica explícita pelo nome do método.

Quando usar: criação de objetos com muitos campos opcionais, ou quando a ordem de configuração importa e precisa ser legível.

Decorator

Adicionar comportamento a um objeto sem alterar sua implementação. O decorator envolve o objeto original e adiciona lógica antes ou depois da chamada.

LoggingRepository(
  CachingRepository(
    SqlRepository()
  )
)

Cada camada adiciona uma responsabilidade isolada: logging, cache, retry (nova tentativa), rate limiting (limitação de taxa de requisições). A composição é feita na configuração, não espalhada pelo código. A implementação original não sabe que está sendo decorada.

Quando usar: comportamento transversal (logging, cache, autenticação) que precisa ser aplicado de forma composável, sem modificar a implementação base.

Singleton

Uma única instância de uma classe durante todo o ciclo de vida da aplicação. Qualquer parte do código que solicita a dependência recebe a mesma instância.

Config (instância única)
  → carregada uma vez no startup
  → compartilhada por todos os módulos
  → nunca reinstanciada

Quando usar: estado genuinamente global e sem variação por contexto: pool de conexões, configuração da aplicação, logger compartilhado. Evitar em lógica de domínio: oculta dependências e dificulta testes.

Adapter

Dois componentes com interfaces incompatíveis precisam colaborar. O Adapter envolve um dos dois e traduz a interface para o formato que o outro espera, sem modificar nenhum dos dois.

EmailService (interno)
  → adapter
SendGridClient (externo)

O código de domínio chama EmailService.send(). O adapter traduz para a API (Application Programming Interface, Interface de Programação de Aplicações) do SendGrid. Trocar o provedor é trocar o adapter, sem tocar o domínio.

Quando usar: integrar bibliotecas externas, APIs de terceiros ou código legado com interface diferente da esperada pelo domínio.

Facade

Um subsistema com muitos componentes expõe complexidade desnecessária para quem só precisa de uma operação de alto nível. Facade cria uma interface simplificada que coordena o subsistema internamente.

OrderFacade.place(cart)
PaymentService.charge()
InventoryService.reserve()
EmailService.confirmOrder()
  → retorna OrderConfirmation

O caller usa uma única entrada. O subsistema pode crescer internamente sem que a interface pública mude.

Quando usar: orquestrar múltiplos serviços em uma operação de negócio, ou simplificar acesso a uma biblioteca com muitos pontos de entrada.

Proxy

Um substituto que intercepta o acesso a outro objeto. O Proxy implementa a mesma interface que o objeto real e decide o que acontece antes, depois ou no lugar da chamada.

UserRepositoryProxy (cache)
  → verifica cache local
HIT: retorna sem acessar o banco
MISS: delega para SqlUserRepository → armazena no cache

O caller não sabe que está falando com um proxy. A lógica de cache, controle de acesso ou logging fica isolada do objeto real.

Quando usar: cache transparente, controle de acesso por permissão, logging de chamadas sem modificar o objeto real, ou lazy initialization (inicialização tardia) de recursos pesados.

Chain of Responsibility

Uma requisição passa por uma cadeia de handlers (processadores). Cada handler decide se processa a requisição ou a passa para o próximo. O caller não sabe qual handler vai processar.

Request
AuthHandler (valida token)
RateLimitHandler (verifica limite de taxa)
ValidationHandler (valida payload)
BusinessHandler (executa lógica)

Adicionar um novo passo é adicionar um novo handler e inseri-lo na cadeia. A ordem é explícita na configuração.

Quando usar: pipelines de middleware (componente de pipeline), validação em múltiplas etapas, processamento de eventos onde os passos precisam ser montados de forma composável.

Command

Encapsula uma operação como um objeto. O Command carrega os parâmetros, o executor e o contexto necessário para executar a operação em qualquer momento.

PlaceOrderCommand { orderId, userId, items }
  → armazenado na fila
  → executado pelo Worker
  → resultado auditado

Separar a criação do comando da sua execução permite filas de operações, retry (nova tentativa), undo/redo e auditoria de ações.

Quando usar: operações que precisam ser enfileiradas, agendadas, revertidas ou auditadas. Complementa CQRS: os Commands do write side (lado de escrita) são objetos que encapsulam a intenção de mudança de estado.

State

O comportamento de um objeto muda conforme seu estado interno. Sem o padrão, cada método acumula um if/switch verificando o estado atual, com lógica crescendo sem controle.

State extrai cada estado em sua própria implementação:

Order
  ├── PendingState   → permite: pay(), cancel()
  ├── PaidState      → permite: ship(), refund()
  └── ShippedState   → permite: deliver(), return()

O objeto delega para o estado atual. Adicionar um novo estado é adicionar uma nova implementação, sem tocar os estados existentes.

Quando usar: entidades com ciclo de vida explícito (pedidos, contratos, workflows) onde cada estado permite ações distintas.

Template Method

Um algoritmo tem etapas fixas e etapas que variam por implementação. Template Method define o esqueleto na classe base e deixa cada subclasse preencher as etapas variáveis.

ReportGenerator (base)
fetchData()       ← implementado por cada subclasse
format(data)      ← implementado por cada subclasse
export(result)    ← fixo na base

PdfReportGeneratorfetchData() + format() específicos para PDF
CsvReportGeneratorfetchData() + format() específicos para CSV

A sequência é controlada pela base. As variações ficam nas subclasses sem duplicar a estrutura do algoritmo.

Quando usar: algoritmos com estrutura fixa e etapas variáveis por tipo: geração de relatórios, processamento de arquivos, pipelines de importação com formatos diferentes.

CQRS: Command Query Responsibility Segregation

Não confundir com CQS (Command-Query Separation), que é um princípio de função: a função retorna valor ou produz efeito, nunca os dois. CQRS é um padrão arquitetural que separa modelos inteiros de escrita e leitura.

Em sistemas com lógica de negócio complexa, o modelo de escrita (validações, invariantes, regras de domínio) e o modelo de leitura (relatórios, dashboards, listas paginadas) divergem: o que faz sentido para persistir não é o que faz sentido para exibir.

CQRS separa os dois em modelos distintos:

Command (escrita)              Query (leitura)
─────────────────              ───────────────
CreateOrder                    GetOrderSummary
  → valida domínio               → lê projeção desnormalizada
  → persiste no write model      → retorna DTO otimizado para a UI
  → emite evento

O write model (modelo de escrita) aplica as regras de domínio e persiste o estado. O read model (modelo de leitura), chamado de Projection (Projeção), é uma visão desnormalizada e otimizada para consulta. Pode ser uma tabela separada, uma view materializada ou um índice de busca.

ResponsabilidadeModeloObjetivo
CommandWrite modelValidar e persistir mudança de estado
QueryRead model (Projection)Servir dados otimizados para leitura

Quando usar: sistemas onde o modelo de leitura e o de escrita divergem de forma significativa: relatórios complexos, dashboards de alto volume, auditoria, histórico de eventos. Em CRUDs simples, CQRS é overhead sem benefício.

AI-Driven Development (Desenvolvimento Assistido por IA)

Desenvolvimento assistido por LLM (Large Language Model, Modelo de Linguagem de Grande Escala) integrado ao ciclo de engenharia: geração de código, revisão, sugestão de refactoring e navegação em bases de código grandes.

O risco central não é a IA: é a ausência de revisão crítica. Código gerado sem avaliação contra a spec e os padrões do projeto cria dívida técnica opaca: funciona, mas não se encaixa no modelo de domínio, ignora convenções ou duplica lógica existente.

A prática correta:

Spec define o contrato → IA gera o candidato → Engenheiro revisa contra spec e padrões → Merge

Nesse modelo, a IA acelera a geração; o engenheiro mantém a responsabilidade pelo design e pela qualidade. A spec é o critério de avaliação, não o feeling de "parece certo".

Quando usar: qualquer tarefa onde o contrato já está definido. A IA produz melhor quando sabe o que deve entregar; tarefas sem spec clara geram código sem critério de aceitação.

SDD: Spec-Driven Development (Desenvolvimento Orientado a Especificações)

A spec (especificação) define entradas, saídas e comportamentos esperados antes de qualquer linha de implementação. O código serve a spec, não o contrário.

Ciclo:

SPECPLANCODETESTEND
  • SPEC: define o contrato: o quê e por quê, não o como
  • PLAN: decompõe em tarefas ordenadas com esforço estimado
  • CODE: implementa o plano, nada além
  • TEST: verifica que a implementação satisfaz a spec
  • END: fecha o ciclo com changelog, backlog sync e commit

O benefício central é custo de decisão: rever uma spec é grátis; rever código já implementado tem custo de entendimento, reescrita e reteste. Decisões de design tomadas na spec chegam ao código com clareza de intenção.

Este guia segue SDD. Referência completa do padrão: specdrivenguide.org.

Desenvolvido por @thiagocajadev · Fork baseado no repositório pmndrs/docs · Poimandres.