Design Patterns

Builder

Separa a construção de objetos complexos de sua representação

Conceito e intenção

Builder constrói objetos complexos passo a passo, separando a lógica de construção da representação final. O padrão é especialmente útil quando o objeto tem muitos parâmetros — especialmente opcionais — e quando se quer garantir que o objeto só exista em estado válido e imutável.

O problema motivador é o construtor telescópico: à medida que os parâmetros crescem, criam-se múltiplas sobrecargas (new Order(id, customer), new Order(id, customer, discount), new Order(id, customer, discount, coupon) …). Isso é ilegível e propenso a erros de posição. A alternativa com setters cria objetos mutáveis que podem ser usados incompletos.

Builder elimina os dois problemas: o objeto só nasce quando .build() é chamado (e as validações rodam nesse momento), e pode ser imutável porque nunca é exposto em estado parcial.


Estrutura

┌──────────────┐      ┌─────────────────────┐
│   Director   │─────▶│ <<interface>>       │
│  (opcional)  │      │     Builder         │
└──────────────┘      │  + setPartA()       │
                      │  + setPartB()       │
                      │  + build(): Product │
                      └──────────┬──────────┘

                        ┌────────┴────────┐
                        │ ConcreteBuilder │
                        │  - fields...    │
                        │  + build()      │
                        └─────────────────┘

Participantes:

  • Builder — interface com métodos de configuração
  • ConcreteBuilder — acumula estado e cria o produto em build()
  • Product — o objeto imutável resultante
  • Director — (opcional) orquestra sequências de build reutilizáveis

Implementação Java — fluent API com inner class

public final class OrderRequest {
    // Campos imutáveis
    private final String customerId;
    private final List<OrderItem> items;
    private final String couponCode;         // opcional
    private final Address shippingAddress;
    private final PaymentMethod paymentMethod;
    private final boolean expressDelivery;   // padrão: false
    private final String notes;              // opcional

    // Construtor privado — só Builder pode chamar
    private OrderRequest(Builder builder) {
        this.customerId      = builder.customerId;
        this.items           = List.copyOf(builder.items);
        this.couponCode      = builder.couponCode;
        this.shippingAddress = builder.shippingAddress;
        this.paymentMethod   = builder.paymentMethod;
        this.expressDelivery = builder.expressDelivery;
        this.notes           = builder.notes;
    }

    // Getters somente — sem setters
    public String getCustomerId()           { return customerId; }
    public List<OrderItem> getItems()       { return items; }
    public Optional<String> getCouponCode() { return Optional.ofNullable(couponCode); }
    public Address getShippingAddress()     { return shippingAddress; }
    public PaymentMethod getPaymentMethod() { return paymentMethod; }
    public boolean isExpressDelivery()      { return expressDelivery; }
    public Optional<String> getNotes()      { return Optional.ofNullable(notes); }

    public static Builder builder(String customerId) {
        return new Builder(customerId);
    }

    public static final class Builder {
        private final String customerId;
        private final List<OrderItem> items = new ArrayList<>();
        private String couponCode;
        private Address shippingAddress;
        private PaymentMethod paymentMethod;
        private boolean expressDelivery = false;
        private String notes;

        private Builder(String customerId) {
            this.customerId = Objects.requireNonNull(customerId, "customerId é obrigatório");
        }

        public Builder addItem(String sku, int quantity) {
            this.items.add(new OrderItem(sku, quantity));
            return this;
        }

        public Builder coupon(String couponCode) {
            this.couponCode = couponCode;
            return this;
        }

        public Builder shippingAddress(Address address) {
            this.shippingAddress = address;
            return this;
        }

        public Builder paymentMethod(PaymentMethod method) {
            this.paymentMethod = method;
            return this;
        }

        public Builder expressDelivery() {
            this.expressDelivery = true;
            return this;
        }

        public Builder notes(String notes) {
            this.notes = notes;
            return this;
        }

        // Validação centralizada antes de criar o objeto
        public OrderRequest build() {
            if (items.isEmpty()) {
                throw new IllegalStateException("Pedido deve ter pelo menos um item");
            }
            Objects.requireNonNull(shippingAddress, "Endereço de entrega é obrigatório");
            Objects.requireNonNull(paymentMethod, "Método de pagamento é obrigatório");
            return new OrderRequest(this);
        }
    }
}

// Uso — legível, sem ambiguidade de posição
OrderRequest order = OrderRequest.builder("customer-123")
    .addItem("PROD-001", 2)
    .addItem("PROD-002", 1)
    .coupon("DESCONTO10")
    .shippingAddress(Address.of("Rua das Flores", "123", "São Paulo"))
    .paymentMethod(PaymentMethod.CREDIT_CARD)
    .expressDelivery()
    .notes("Deixar com porteiro")
    .build();

Builder com validação avançada e step builder

Step Builder força a sequência obrigatória de chamadas em tempo de compilação:

// Interfaces representam os "passos" obrigatórios
public interface CustomerStep    { AddressStep forCustomer(String customerId); }
public interface AddressStep     { PaymentStep deliverTo(Address address); }
public interface PaymentStep     { FinalStep withPayment(PaymentMethod method); }
public interface FinalStep {
    FinalStep addItem(String sku, int qty);
    FinalStep coupon(String code);
    FinalStep expressDelivery();
    OrderRequest build();
}

// Implementação única que implementa todas as interfaces
public class OrderRequestStepBuilder
    implements CustomerStep, AddressStep, PaymentStep, FinalStep {
    // campos e implementação dos métodos...
}

// Uso — o compilador impede ordem errada
OrderRequest order = OrderRequest.steps()         // retorna CustomerStep
    .forCustomer("customer-123")                  // retorna AddressStep
    .deliverTo(address)                           // retorna PaymentStep
    .withPayment(PaymentMethod.CREDIT_CARD)       // retorna FinalStep
    .addItem("PROD-001", 2)
    .build();

// Erro de compilação: não dá para chamar .build() antes de forCustomer()

Lombok @Builder

Em projetos com Lombok, o boilerplate é gerado automaticamente:

@Builder
@Value  // imutável: todos os campos final, getters gerados
public class OrderRequest {
    @NonNull String customerId;
    @Singular List<OrderItem> items;   // gera addItem() e items()
    String couponCode;
    @NonNull Address shippingAddress;
    @NonNull PaymentMethod paymentMethod;
    @Builder.Default boolean expressDelivery = false;
    String notes;
}

// Uso idêntico ao manual
OrderRequest order = OrderRequest.builder()
    .customerId("customer-123")
    .item(new OrderItem("PROD-001", 2))
    .item(new OrderItem("PROD-002", 1))
    .shippingAddress(address)
    .paymentMethod(PaymentMethod.CREDIT_CARD)
    .build();

Para validação customizada com Lombok, use @Builder + método build() manual via @Builder(buildMethodName = "create") e método público build() que valida antes de chamar create().


Implementação TypeScript

// Versão TypeScript com fluent interface
class OrderRequest {
  readonly customerId: string;
  readonly items: ReadonlyArray<OrderItem>;
  readonly couponCode?: string;
  readonly shippingAddress: Address;
  readonly paymentMethod: PaymentMethod;
  readonly expressDelivery: boolean;
  readonly notes?: string;

  private constructor(builder: OrderRequestBuilder) {
    this.customerId      = builder['_customerId'];
    this.items           = [...builder['_items']];
    this.couponCode      = builder['_couponCode'];
    this.shippingAddress = builder['_shippingAddress']!;
    this.paymentMethod   = builder['_paymentMethod']!;
    this.expressDelivery = builder['_expressDelivery'];
    this.notes           = builder['_notes'];
  }

  static builder(customerId: string): OrderRequestBuilder {
    return new OrderRequestBuilder(customerId);
  }
}

class OrderRequestBuilder {
  private _customerId: string;
  private _items: OrderItem[] = [];
  private _couponCode?: string;
  private _shippingAddress?: Address;
  private _paymentMethod?: PaymentMethod;
  private _expressDelivery = false;
  private _notes?: string;

  constructor(customerId: string) {
    if (!customerId) throw new Error('customerId é obrigatório');
    this._customerId = customerId;
  }

  addItem(sku: string, quantity: number): this {
    this._items.push({ sku, quantity });
    return this;
  }

  coupon(code: string): this        { this._couponCode = code; return this; }
  deliverTo(addr: Address): this    { this._shippingAddress = addr; return this; }
  payWith(pm: PaymentMethod): this  { this._paymentMethod = pm; return this; }
  express(): this                   { this._expressDelivery = true; return this; }
  withNotes(notes: string): this    { this._notes = notes; return this; }

  build(): OrderRequest {
    if (this._items.length === 0)  throw new Error('Pedido sem itens');
    if (!this._shippingAddress)    throw new Error('Endereço obrigatório');
    if (!this._paymentMethod)      throw new Error('Pagamento obrigatório');
    return new (OrderRequest as any)(this);
  }
}

// Uso
const order = OrderRequest.builder('customer-123')
  .addItem('PROD-001', 2)
  .deliverTo({ street: 'Rua das Flores', number: '123', city: 'SP' })
  .payWith('CREDIT_CARD')
  .express()
  .build();

No mundo real

StringBuilder / StringBuffer no Java SDK são builders clássicos — append(), insert(), delete() retornam this; toString() é o equivalente de build().

HttpRequest.Builder (Java 11 java.net.http) — fluent builder para construir requisições HTTP: .uri(), .header(), .POST(body), .timeout(), .build().

Spring MockMvcRequestBuilders em testes:

mockMvc.perform(post("/orders")
    .contentType(APPLICATION_JSON)
    .header("Authorization", token)
    .content(json))
    .andExpect(status().isCreated());

UriComponentsBuilder do Spring — builder para construir URLs de forma segura, evitando concatenação de strings com query params.

Criteria / CriteriaBuilder do JPA — builder para queries dinâmicas sem concatenação de JPQL.

RestTemplate e WebClient (Spring WebFlux) usam builders para configuração:

WebClient client = WebClient.builder()
    .baseUrl("https://api.example.com")
    .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .filter(ExchangeFilterFunctions.basicAuthentication(user, pass))
    .build();

Kotlin DSL / Gradle usa builders extensivamente — dependencies { implementation(...) } é açúcar sintático sobre builders de configuração.


Quando usar

  • Objetos com 4 ou mais parâmetros, especialmente quando muitos são opcionais
  • Objetos que precisam ser imutáveis (sem setters) mas têm construção complexa
  • Quando a ordem dos parâmetros é ambígua (dois String, dois boolean do mesmo tipo)
  • Quando a validação deve ocorrer no momento de criação, não após
  • Quando o mesmo processo de construção deve produzir representações diferentes (Director + Builders distintos)

Quando NÃO usar

  • Objetos simples com 1-3 parâmetros bem definidos — construtor direto é mais legível
  • Quando Lombok @Builder ou Records (Java 14+) já resolvem o problema sem boilerplate manual
  • Quando o objeto é mutável por design — Builder não acrescenta valor para objetos que precisam de setters
  • Quando a ordem dos passos não tem semântica — se qualquer ordem é válida, um construtor com parâmetros nomeados (Kotlin, Python) é equivalente sem a cerimônia do builder

Combinações com outros padrões

Builder + Factory Method: A Factory decide qual Builder instanciar; o Builder cuida da construção complexa. Útil quando há múltiplas variações do produto:

OrderRequestBuilder builder = builderFactory.create(orderType); // factory
OrderRequest order = builder.withDefaults(customer).addItem(item).build(); // builder

Builder + Composite: Quando o produto é uma estrutura em árvore (documento HTML, configuração aninhada), o Builder pode construir nós recursivamente, formando um Composite.

Builder + Template Method: O método build() pode seguir o padrão Template Method — executa passos fixos (validar, normalizar, criar) mas permite que subclasses do Builder sobrescrevam etapas específicas.