O balcão de sanduíches
Você não entrega uma lista de doze ingredientes de uma vez ao atendente — escolhe o pão, depois a proteína, pula o que não quer, acrescenta extras. Cada escolha é opcional e tem nome claro. O atendente (builder) acumula os passos e monta na ordem certa; o sanduíche só existe quando você diz 'pode fechar'. Ninguém recebe um sanduíche pela metade nem precisa decorar em que posição da lista vai o queijo.
O problema
Objetos com muitos parâmetros opcionais empurram o código para o telescoping constructor: uma escada de construtores sobrecarregados ou chamadas como new Pedido(itens, null, true, null), em que ninguém sabe o que cada posição significa.
A alternativa ingênua — criar vazio e ir preenchendo com setters — deixa o objeto existir em estados intermediários inválidos, e a validação se espalha por todos os pontos de uso.
O nome 'telescoping constructor' vem da forma: construtor de 2 parâmetros chama o de 3, que chama o de 4, como um telescópio se abrindo. Cada combinação de opcionais pede mais uma sobrecarga, e o chamador acaba passando null/false posicionais para alcançar o parâmetro que interessa. É o cheiro clássico que o Builder resolve — popularizado nessa forma fluente por Joshua Bloch no Effective Java.
Vale distinguir as duas encarnações do padrão. O Builder clássico do GoF separa construção de representação: um Director executa os mesmos passos sobre builders diferentes para produzir produtos diferentes (o mesmo roteiro gera relatório HTML ou PDF). O builder fluente do dia a dia ataca outro problema — legibilidade e validação de objetos com muitos opcionais — e raramente tem Director. Os dois compartilham o mecanismo: acumular estado parcial e entregar o produto no final.
Honestidade necessária: em linguagens com parâmetros nomeados e valores default (Python, Kotlin, C#) ou objeto literal tipado (TypeScript), boa parte dos casos dispensa builder — new Pedido({ itens, embrulho: true }) já é legível. O builder volta a valer a pena quando há validação entre campos, passos que precisam de ordem, ou quando você quer imutabilidade com montagem incremental.
- Cliente
- encadeia passos com nomeBuilder
- acumula escolhasEstado parcial
- construir() valida e montaObjeto completo
Navegue pelas camadas
Camada 1/4Cliente
use ← →Camada 1
Cliente
encadeia só os passos que precisa
Lê como uma frase: cada passo tem nome de negócio (comCupom, paraPresente) e os opcionais simplesmente não aparecem. A ordem dos passos costuma ser livre — quem garante a coerência é o construir(), não o chamador.
No código
new PedidoBuilder()
.adicionarItem("Camiseta P")
.paraPresente()Se violar: cliente que monta o objeto por setters depois do construir() reabre a janela de estados inválidos que o builder fechou.
Estrutura
Código
class Pedido {
constructor(
readonly itens: string[],
readonly cupom?: string,
readonly embrulho = false,
) {}
}
// Sem builder: new Pedido(itens, undefined, true)
// — quem lê não sabe o que o "true" significa
class PedidoBuilder {
private itens: string[] = [];
private cupom?: string;
private embrulho = false;
adicionarItem(item: string): this {
this.itens.push(item);
return this; // fluent: cada passo devolve o builder
}
comCupom(codigo: string): this {
this.cupom = codigo;
return this;
}
paraPresente(): this {
this.embrulho = true;
return this;
}
construir(): Pedido {
if (this.itens.length === 0) throw new Error("Pedido sem itens");
return new Pedido([...this.itens], this.cupom, this.embrulho);
}
}
const pedido = new PedidoBuilder()
.adicionarItem("Camiseta P")
.comCupom("BEMVINDO10")
.paraPresente()
.construir(); // só aqui o Pedido passa a existir
console.log(pedido.embrulho); // true — e o nome do passo explica o porquêclass Pedido:
def __init__(self, itens, cupom=None, embrulho=False):
self.itens = itens
self.cupom = cupom
self.embrulho = embrulho
class PedidoBuilder:
def __init__(self):
self._itens = []
self._cupom = None
self._embrulho = False
def adicionar_item(self, item):
self._itens.append(item)
return self # fluent: cada passo devolve o builder
def com_cupom(self, codigo):
self._cupom = codigo
return self
def para_presente(self):
self._embrulho = True
return self
def construir(self):
if not self._itens:
raise ValueError("Pedido sem itens")
return Pedido(list(self._itens), self._cupom, self._embrulho)
pedido = (
PedidoBuilder()
.adicionar_item("Camiseta P")
.com_cupom("BEMVINDO10")
.para_presente()
.construir() # só aqui o Pedido passa a existir
)
print(pedido.embrulho) # TrueCasos de uso
Cobrança Pix com juros, multa e desconto
Cenário
Uma fintech emite cobranças com vencimento em que quase tudo é opcional — juros, multa, desconto por antecipação — mas as combinações têm regras: desconto só antes do vencimento, multa só depois.
Como o padrão entra
Um CobrancaBuilder expõe um passo nomeado por campo opcional e o construir() valida as combinações antes de chamar o provedor. O código de emissão lê como a regra de negócio, e cobrança inválida nem chega a existir como objeto.
Trade-off: O builder vira o guardião das regras de combinação — quando a API do provedor muda uma regra, é preciso lembrar que a validação vive no builder, não só no contrato externo.
Query builder de relatórios
Cenário
Um SaaS de analytics monta consultas com filtros, ordenação, agrupamento e paginação opcionais — é o mesmo problema que Knex e o construtor de queries do Prisma resolvem em produção.
Como o padrão entra
Cada tela encadeia só os passos que usa (porPeriodo, agrupadoPor, ordenadoPor) e o construir() gera o SQL/objeto de consulta coerente. Combinações inválidas (agrupamento sem métrica) falham na montagem, não no banco.
Trade-off: O builder garante consulta sintaticamente válida, não consulta boa: continua fácil encadear passos que geram um plano de execução péssimo — ele não substitui a revisão do SQL gerado.
Test data builders na suíte de testes
Cenário
Os testes de um e-commerce precisam de pedidos em dezenas de variações — pago, cancelado, com N itens, com cupom — e montar cada um na mão duplica detalhes irrelevantes em todo teste.
Como o padrão entra
Um umPedido() com defaults válidos e passos como .pago() e .comItens(3) deixa cada teste declarar apenas o que importa para o cenário. Mudou o modelo? Ajusta-se o builder, não os duzentos testes.
Trade-off: Os defaults escondidos podem mascarar o que o teste realmente exige — um teste que passa por causa de um default do builder é um teste que mente sobre o próprio cenário.
- 1
Acumular no builder
Campos privados mutáveis e métodos fluentes que devolvem `this`. Só o builder pode estar 'pela metade'.
- 2
Validar em construir()
Invariantes que cruzam campos (itens não vazios, desconto compatível com vencimento) falham aqui — ou o objeto não nasce.
- 3
Entregar imutável
O produto recebe tudo no construtor e não expõe setters. Quem tem um `Pedido` tem a garantia da validação.
- 4
Um builder por produto
Reusar a mesma instância após `construir()` vaza cupom e itens do pedido anterior. Crie outro ou resete com clareza.
O Builder que não valida e entrega mutável
Compila, roda, passa no code review. E quebra depois.
A API é fluente e o nome é Builder — mas construir() aceita qualquer coisa e o produto ainda tem addItem. A montagem bonita não fechou a janela de estados inválidos.
class PedidoBuilder {
private itens: string[] = [];
private cupom?: string;
adicionarItem(item: string): this {
this.itens.push(item);
return this;
}
comCupom(codigo: string): this {
this.cupom = codigo;
return this;
}
// "Builder"... sem validacao e com produto mutavel.
construir(): Pedido {
return new Pedido(this.itens, this.cupom); // pedido vazio passa
}
}
class Pedido {
constructor(public itens: string[], public cupom?: string) {}
addItem(i: string) { this.itens.push(i); } // <- reabre estado invalido
}
const p = new PedidoBuilder().construir(); // Pedido sem itens
p.addItem("camiseta"); // invariante validada? nunca.O que quebra
- Com zero passosUm pedido sem itens circula no sistema; o erro aparece longe, no gateway ou no PDF.
- Depois do construirQualquer código muta o produto e as invariantes 'validadas' deixam de ser garantia.
- Na leituraNinguém sabe se precisa passar pelo builder ou se pode
new Pedido([])direto — dois caminhos, duas regras.
A saída: construir() recusa pedido vazio; o produto nasce com readonly e sem setters. O builder é o único caminho de criação — senão é só açúcar sintático.
Armadilhas
construir() que não valida
Se qualquer sequência de passos produz um objeto — inclusive um quebrado —, o builder é só sintaxe bonita em volta do mesmo bug. As invariantes que cruzam campos (itens não vazios, desconto compatível com vencimento) pertencem ao construir(); as de campo único podem falhar já no próprio passo.
Builder reutilizado entre produtos
Chamar construir() duas vezes na mesma instância vaza estado do primeiro objeto para o segundo — o cupom do pedido anterior aparece no próximo. Crie um builder por produto, ou faça o construir() resetar/invalidar a instância explicitamente.
Builder para objeto de três campos
Em TypeScript, new Pedido({ itens, embrulho: true }) com um objeto literal tipado dá legibilidade de graça; em Python, parâmetros nomeados idem. Builder ali é uma classe inteira para resolver um problema que a linguagem já resolveu — reserve-o para quando houver validação entre passos ou montagem incremental de verdade.
Quando usar × quando evitar
Use quando
4- O objeto tem muitos parâmetros opcionais ou combináveis e os construtores viraram uma escada ilegível.
- A construção exige validação entre campos ou passos ordenados antes de o objeto poder existir.
- Você quer um produto imutável com montagem incremental (dados chegam aos poucos).
- Os mesmos passos de construção produzem representações diferentes (Director + builders distintos).
Evite quando
3- Objetos com dois ou três campos autoexplicativos — cerimônia pura.
- A linguagem resolve com parâmetros nomeados/default ou objeto literal tipado e não há validação entre passos.
- O 'builder' seria só um espelho de setters sem validação — um objeto de configuração comunica melhor.
Testar o que ficou
3 armadilhas de Builder viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
Query builders (Knex, Prisma).where().orderBy().limit()monta a consulta por partes e só no.then()produz o SQL final.URLSearchParamsVocê acrescenta parâmetro a parâmetro e otoString()no fim entrega a query string pronta.Request do fetchMétodo, headers, body e sinal vão sendo compostos até virar um objeto imutável de requisição.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Monta objeto complexo passo a passo.
const url = new URLSearchParams().set("q", termo).set("page", "1");O que ele cobra
1 salto a mais entre a chamada e o efeito.
- Um objeto builder a manter em paralelo ao objeto que ele constrói
- A construção passa a ser uma sequência de chamadas em vez de um construtor só
Não vale se: o objeto tem poucos campos e um construtor legível já resolve — o builder só compensa quando a montagem é complexa ou muito opcional.