6 min read
¿Alguna vez llamaste a un constructor con ocho o diez parámetros y tuviste que contar con el dedo cuál posición correspondía a cuál campo? new HttpRequest("GET", url, null, null, true, 30, null, false, 3) — ¿cuál de esos booleanos era followRedirects y cuál era verifySSL? Sin abrir la definición de la clase, es imposible saberlo con certeza.
Este problema tiene nombre: constructor telescópico (telescoping constructor), el antipatrón que surge cuando una clase tiene muchos campos opcionales y terminamos creando una cadena de constructores sobrecargados, cada uno con un parámetro más que el anterior. El Patrón Builder (Constructor) es la solución estándar.
¿Qué es el Patrón Builder?
Builder separa la construcción de un objeto complejo de su representación final. En lugar de un constructor gigante, exponés una API fluida (fluent API) donde cada método configura un campo y retorna el mismo builder, permitiendo encadenar llamadas. Al final, un método build() ensambla el objeto completo.
La ventaja no es solo legibilidad — aunque eso ya vale la pena. También es inmutabilidad segura: el objeto final puede ser completamente inmutable (todos sus campos final), algo mucho más difícil de lograr con setters tradicionales, y podés validar reglas de negocio en el momento de construir, no después.
El diagrama

Guía paso a paso
Paso 1 — Identificar los campos obligatorios y opcionales
Los campos obligatorios van en el constructor del builder (o en un método de fábrica estático). Los opcionales se configuran con métodos individuales.
Paso 2 — Crear la clase Builder anidada
Generalmente vive como clase estática interna de la clase que construye, para que ambas evolucionen juntas.
Paso 3 — Cada método setter retorna this
Esto es lo que habilita el encadenamiento fluido de llamadas.
Paso 4 — Validar en el método build()
Antes de construir el objeto final, verificá que las combinaciones de campos tengan sentido (por ejemplo, que no falten campos obligatorios).
Paso 5 — Hacer el objeto final inmutable
Todos los campos de la clase construida deberían ser final, sin setters públicos. Una vez construido, no cambia.
El código
El ejemplo modela la construcción de una solicitud HTTP configurable — un caso real presente en casi cualquier cliente HTTP hecho a mano. Aunque Lombok ofrece @Builder automático, lo implementamos manualmente primero para entender qué genera por detrás.
import java.util.HashMap;
import java.util.Map;
// --- La clase construida: completamente inmutable ---
class HttpRequest {
private final String method;
private final String url;
private final Map<String, String> headers;
private final String body;
private final int timeoutSeconds;
private final int maxRetries;
private final boolean followRedirects;
// Constructor privado: solo el Builder puede crear instancias
private HttpRequest(Builder builder) {
this.method = builder.method;
this.url = builder.url;
this.headers = builder.headers;
this.body = builder.body;
this.timeoutSeconds = builder.timeoutSeconds;
this.maxRetries = builder.maxRetries;
this.followRedirects = builder.followRedirects;
}
public String getMethod() { return method; }
public String getUrl() { return url; }
public Map<String, String> getHeaders() { return headers; }
public String getBody() { return body; }
public int getTimeoutSeconds() { return timeoutSeconds; }
public int getMaxRetries() { return maxRetries; }
public boolean isFollowRedirects() { return followRedirects; }
@Override
public String toString() {
return method + " " + url + " (timeout=" + timeoutSeconds + "s, retries=" + maxRetries + ")";
}
// --- El Builder ---
public static class Builder {
// Obligatorios
private final String method;
private final String url;
// Opcionales, con valores por defecto sensatos
private Map<String, String> headers = new HashMap<>();
private String body = null;
private int timeoutSeconds = 30;
private int maxRetries = 0;
private boolean followRedirects = true;
public Builder(String method, String url) {
if (method == null || url == null) {
throw new IllegalArgumentException("method y url son obligatorios");
}
this.method = method;
this.url = url;
}
public Builder header(String key, String value) {
this.headers.put(key, value);
return this;
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder timeout(int seconds) {
this.timeoutSeconds = seconds;
return this;
}
public Builder retries(int maxRetries) {
this.maxRetries = maxRetries;
return this;
}
public Builder followRedirects(boolean follow) {
this.followRedirects = follow;
return this;
}
public HttpRequest build() {
// Validación de reglas de negocio antes de construir
if (method.equals("POST") && body == null) {
throw new IllegalStateException("Las solicitudes POST requieren un body");
}
return new HttpRequest(this);
}
}
}
// --- Main ---
public class BuilderPatternDemo {
public static void main(String[] args) {
// Solicitud simple: solo lo obligatorio, el resto usa defaults
HttpRequest simpleRequest = new HttpRequest.Builder("GET", "https://api.ejemplo.com/users")
.build();
System.out.println(simpleRequest);
// Solicitud compleja: configuración completa, legible campo por campo
HttpRequest complexRequest = new HttpRequest.Builder("POST", "https://api.ejemplo.com/orders")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body("{\"productId\":\"P-100\",\"quantity\":2}")
.timeout(15)
.retries(3)
.followRedirects(false)
.build();
System.out.println(complexRequest);
// Intento inválido: POST sin body — falla en build(), no en tiempo de ejecución más tarde
try {
new HttpRequest.Builder("POST", "https://api.ejemplo.com/orders").build();
} catch (IllegalStateException e) {
System.out.println("Error esperado: " + e.getMessage());
}
}
}
Output esperado:
GET https://api.ejemplo.com/users (timeout=30s, retries=0)
POST https://api.ejemplo.com/orders (timeout=15s, retries=3)
Error esperado: Las solicitudes POST requieren un body
Para tener en cuenta en producción
- Lombok automatiza todo el boilerplate: con
@Buildersobre la clase, Lombok genera el Builder completo (aunque sin la validación de negocio personalizada, que seguís escribiendo vos). Combinado con@Getter, es la forma más común de usar este patrón en proyectos Java modernos. - Cuidado con Builders mutables compartidos entre threads: un objeto Builder no es thread-safe por defecto. Si varios threads reutilizan la misma instancia de Builder, vas a tener condiciones de carrera. Cada thread debería crear su propio Builder.
- No abuses del patrón en clases simples: si tu clase tiene dos o tres campos, todos obligatorios, un constructor normal es más simple y directo. Builder brilla cuando hay muchos campos opcionales o combinaciones que requieren validación.
- Conecta con Método Fábrica: si ya leíste el post de Fábrica Método, notarás la diferencia de propósito: Fábrica decide qué tipo de objeto crear, Builder decide cómo ensamblar un objeto con muchas partes configurables. Se pueden combinar: una fábrica que retorna distintos Builders preconfigurados según el contexto.
Conclusión
Builder no es un patrón sofisticado — de hecho es de los más intuitivos de aplicar una vez que lo viste en acción. Pero su impacto en la legibilidad del código es enorme: cada llamada encadenada se lee casi como una oración, y el compilador te protege de olvidar un campo obligatorio o de confundir el orden de los parámetros.
Si alguna vez contaste parámetros con el dedo para entender un constructor, ya sabés exactamente qué problema resuelve este patrón.
Recordá suscribirte aquí para recibir los próximos posts directamente en tu correo.
Referencias:
Effective Java — Joshua Bloch
Head First Design Patterns — Eric Freeman, Elisabeth Robson