Builder Pattern & Fluent API
Giriş
Java'da çok parametreli nesneler oluşturmak zahmetli olabilir. 10 parametreli bir constructor gördüğünüzde, hangi parametrenin ne olduğunu anlamak imkansızdır. Setter'lar ise nesneyi mutable (değiştirilebilir) bırakır — oluşturulduktan sonra herkes her alanı değiştirebilir, thread-safety garantisi yoktur.
Bir burger siparişini düşünün: "Bir adet, çift katlı, cheddar peynirli, turşusuz, ketçaplı, büyük boy patates ve orta boy kola" — bunu tek seferde söylemeye çalışmak zordur. Ama adım adım seçim yapmak hem kolay hem de hatasızdır. Builder pattern tam olarak bu adım adım oluşturma yaklaşımını kodda uygular.
Bu derste klasik Builder implementasyonunu, Lombok @Builder kısayolunu, fluent API tasarım prensiplerini ve immutable nesnelerin önemini derinlemesine inceleyeceğiz.
Problem: Teleskopik Constructor
// 12 parametreli constructor — hangi parametre ne?
Order order = new Order(
"ORD-001", // orderId? orderNumber?
42L, // customerId? productId?
"John Doe", // customerName? productName?
"john@example.com", // email
"123 Main St", // address? billingAddress?
"Istanbul", // city
"34000", // zipCode
BigDecimal.valueOf(299.99), // totalAmount? unitPrice?
"PENDING", // status
LocalDateTime.now(), // createdAt? updatedAt?
null, // updatedAt? deletedAt?
true, // express? gift? active?
"EXPRESS" // shippingMethod? paymentMethod?
);Sorunlar:
Okunaksız: Hangi parametrenin ne olduğu belli değil
Hata riski: Parametre sırasını karıştırmak çok kolay (iki String parametreyi yer değiştirseniz derleyici hata vermez)
Opsiyonel parametreler: 12 parametrenin 5'i opsiyonelise, farklı kombinasyonlar için onlarca constructor yazmanız gerekir (teleskopik constructor anti-pattern)
// Teleskopik constructor — her kombinasyon için ayrı constructor
public Order(String orderId, Long customerId) { ... }
public Order(String orderId, Long customerId, String name) { ... }
public Order(String orderId, Long customerId, String name, String email) { ... }
// ... 10 tane dahaAlternatif: JavaBeans (Setter) Pattern
Order order = new Order();
order.setOrderId("ORD-001");
order.setCustomerId(42L);
order.setCustomerName("John Doe");
order.setEmail("john@example.com");
order.setAddress("123 Main St");
order.setCity("Istanbul");
order.setTotalAmount(BigDecimal.valueOf(299.99));
order.setStatus("PENDING");
order.setExpress(true);
order.setShippingMethod("EXPRESS");Daha okunabilir ama ciddi sorunları var:
Mutable: Nesne oluşturulduktan sonra herkes her alanı değiştirebilir
Thread-unsafe: Bir thread setter çağırırken başka bir thread nesneyi okuyabilir
Tutarsız durum: Setter'ların yarısı çağrılmışken nesne yarı-oluşturulmuş halde
Validasyon zorluğu: Tüm setter'lar tamamlanmadan nesnenin geçerli olup olmadığını bilemezsiniz
Builder Pattern — Çözüm
Builder pattern'de nesnenin kendisi immutabledır; oluşturma işlemi iç içe (nested) statik bir Builder sınıfı aracılığıyla yapılır.
Klasik Builder Implementasyonu
public class Order {
// Tüm alanlar final — immutable!
private final String orderId;
private final Long customerId;
private final String customerName;
private final String email;
private final String address;
private final String city;
private final String zipCode;
private final BigDecimal totalAmount;
private final String status;
private final LocalDateTime createdAt;
private final LocalDateTime updatedAt;
private final boolean express;
private final String shippingMethod;
// Private constructor — sadece Builder erişebilir
private Order(Builder builder) {
this.orderId = builder.orderId;
this.customerId = builder.customerId;
this.customerName = builder.customerName;
this.email = builder.email;
this.address = builder.address;
this.city = builder.city;
this.zipCode = builder.zipCode;
this.totalAmount = builder.totalAmount;
this.status = builder.status;
this.createdAt = builder.createdAt;
this.updatedAt = builder.updatedAt;
this.express = builder.express;
this.shippingMethod = builder.shippingMethod;
}
// Getter'lar (setter YOK — immutable!)
public String getOrderId() { return orderId; }
public Long getCustomerId() { return customerId; }
public String getCustomerName() { return customerName; }
public String getEmail() { return email; }
public BigDecimal getTotalAmount() { return totalAmount; }
public String getStatus() { return status; }
public boolean isExpress() { return express; }
// ... diğer getter'lar
// Static factory method
public static Builder builder() {
return new Builder();
}
// Inner static Builder sınıfı
public static class Builder {
// Zorunlu alanlar
private String orderId;
private Long customerId;
// Opsiyonel alanlar — varsayılan değerler
private String customerName;
private String email;
private String address;
private String city;
private String zipCode;
private BigDecimal totalAmount = BigDecimal.ZERO;
private String status = "PENDING";
private LocalDateTime createdAt = LocalDateTime.now();
private LocalDateTime updatedAt;
private boolean express = false;
private String shippingMethod = "STANDARD";
// Her metot Builder döndürür → method chaining
public Builder orderId(String orderId) {
this.orderId = orderId;
return this;
}
public Builder customerId(Long customerId) {
this.customerId = customerId;
return this;
}
public Builder customerName(String customerName) {
this.customerName = customerName;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public Builder city(String city) {
this.city = city;
return this;
}
public Builder zipCode(String zipCode) {
this.zipCode = zipCode;
return this;
}
public Builder totalAmount(BigDecimal totalAmount) {
this.totalAmount = totalAmount;
return this;
}
public Builder express(boolean express) {
this.express = express;
return this;
}
public Builder shippingMethod(String shippingMethod) {
this.shippingMethod = shippingMethod;
return this;
}
// build() — validasyon + nesne oluşturma
public Order build() {
// Zorunlu alan kontrolü
if (orderId == null || orderId.isBlank()) {
throw new IllegalStateException(
"orderId is required");
}
if (customerId == null) {
throw new IllegalStateException(
"customerId is required");
}
if (totalAmount.signum() < 0) {
throw new IllegalStateException(
"totalAmount cannot be negative");
}
return new Order(this);
}
}
}Kullanım — Okunabilir ve Güvenli
// Her parametre adıyla etiketli — ne olduğu açık
Order order = Order.builder()
.orderId("ORD-001")
.customerId(42L)
.customerName("John Doe")
.email("john@example.com")
.address("123 Main St")
.city("Istanbul")
.totalAmount(BigDecimal.valueOf(299.99))
.express(true)
.shippingMethod("EXPRESS")
.build();
// Sadece zorunlu alanlarla (opsiyoneller varsayılan değer alır)
Order simpleOrder = Order.builder()
.orderId("ORD-002")
.customerId(43L)
.totalAmount(BigDecimal.valueOf(49.99))
.build();
// status = "PENDING", express = false, shippingMethod = "STANDARD"Lombok @Builder — Boilerplate Yok
Klasik Builder pattern'ın dezavantajı: çok fazla boilerplate kod. Lombok, tüm bu kodu tek bir anotasyonla ortadan kaldırır:
@Getter
@Builder
@ToString
public class Order {
private final String orderId;
private final Long customerId;
private final String customerName;
private final String email;
private final String address;
private final String city;
private final String zipCode;
private final BigDecimal totalAmount;
@Builder.Default
private final String status = "PENDING";
@Builder.Default
private final LocalDateTime createdAt = LocalDateTime.now();
private final LocalDateTime updatedAt;
@Builder.Default
private final boolean express = false;
@Builder.Default
private final String shippingMethod = "STANDARD";
}@Builder.Default annotation'ı, builder'da set edilmediğinde kullanılacak varsayılan değeri belirtir. Bu olmadan varsayılan değer null (veya primitive türler için 0, false) olur.
Lombok @Builder + Validasyon
Lombok builder'a validasyon eklemek için builder sınıfını kısmen tanımlayabilirsiniz:
@Getter
@Builder
public class Order {
private final String orderId;
private final Long customerId;
private final BigDecimal totalAmount;
@Builder.Default
private final String status = "PENDING";
// Lombok bu sınıfı EXTEND eder — eksik metotları otomatik ekler
public static class OrderBuilder {
public Order build() {
if (orderId == null || orderId.isBlank()) {
throw new IllegalStateException(
"orderId is required");
}
if (customerId == null) {
throw new IllegalStateException(
"customerId is required");
}
// Lombok'un generate ettiği build mantığını çağır
return new Order(orderId, customerId,
totalAmount, status);
}
}
}@Builder + @Singular (Koleksiyon)
@Getter
@Builder
public class EmailMessage {
private final String from;
private final String subject;
private final String body;
@Singular // Liste elemanlarını tek tek ekle
private final List<String> recipients;
@Singular
private final Map<String, String> headers;
@Singular("attachment")
private final List<Attachment> attachments;
}
// Kullanım — tek tek ekleme
EmailMessage email = EmailMessage.builder()
.from("noreply@example.com")
.subject("Siparişiniz onaylandı")
.body("Sipariş detayları...")
.recipient("user@example.com") // tekil
.recipient("manager@example.com") // tekil
.header("X-Priority", "high") // tekil
.header("X-Source", "order-service") // tekil
.attachment(new Attachment("fatura.pdf", pdfBytes))
.build();
// recipients = ["user@example.com", "manager@example.com"]
// headers = {"X-Priority": "high", "X-Source": "order-service"}@Builder + toBuilder (Kopyala ve Değiştir)
@Getter
@Builder(toBuilder = true)
public class Order {
private final String orderId;
private final Long customerId;
private final BigDecimal totalAmount;
@Builder.Default
private final String status = "PENDING";
}
// Mevcut nesneyi kopyala, sadece farklı alanları değiştir
Order original = Order.builder()
.orderId("ORD-001")
.customerId(42L)
.totalAmount(BigDecimal.valueOf(299.99))
.build();
Order updated = original.toBuilder()
.status("SHIPPED") // Sadece status değişir
.build();
// original.status = "PENDING" (DEĞİŞMEDİ — immutable)
// updated.status = "SHIPPED"💡 İpucu:
toBuilder(), immutable nesnelerle çalışırken çok kullanışlıdır. "Kopyala ve değiştir" yaklaşımı ile orijinal nesneye dokunmadan yeni versiyonlar oluşturabilirsiniz.
@Builder + Java Record
Java 17+ record'ları doğal olarak immutable'dır ve Lombok @Builder ile birleştirilebilir:
@Builder
public record OrderSummary(
String orderId,
String customerName,
BigDecimal amount,
OrderStatus status,
LocalDateTime createdAt
) {}
// Kullanım — hem record hem builder avantajları
OrderSummary summary = OrderSummary.builder()
.orderId("ORD-001")
.customerName("John Doe")
.amount(BigDecimal.valueOf(299.99))
.status(OrderStatus.PENDING)
.createdAt(LocalDateTime.now())
.build();Fluent API Tasarımı
Builder pattern'in ötesinde, tüm API'nizi fluent (akıcı) tasarlayabilirsiniz. Temel prensip: her metot this döndürür, böylece çağrılar zincirlenir (method chaining).
QueryBuilder Örneği
public class QueryBuilder {
private String table;
private final List<String> columns = new ArrayList<>();
private final List<String> conditions = new ArrayList<>();
private final List<String> joins = new ArrayList<>();
private String orderBy;
private String orderDirection = "ASC";
private Integer limit;
private Integer offset;
// Static factory — giriş noktası
public static QueryBuilder select(String... columns) {
QueryBuilder qb = new QueryBuilder();
if (columns.length == 0) {
qb.columns.add("*");
} else {
qb.columns.addAll(Arrays.asList(columns));
}
return qb;
}
public QueryBuilder from(String table) {
this.table = table;
return this;
}
public QueryBuilder join(String table, String condition) {
this.joins.add("JOIN " + table + " ON " + condition);
return this;
}
public QueryBuilder leftJoin(String table, String condition) {
this.joins.add("LEFT JOIN " + table + " ON " + condition);
return this;
}
public QueryBuilder where(String condition) {
this.conditions.add(condition);
return this;
}
public QueryBuilder orderBy(String column) {
this.orderBy = column;
return this;
}
public QueryBuilder desc() {
this.orderDirection = "DESC";
return this;
}
public QueryBuilder asc() {
this.orderDirection = "ASC";
return this;
}
public QueryBuilder limit(int limit) {
this.limit = limit;
return this;
}
public QueryBuilder offset(int offset) {
this.offset = offset;
return this;
}
public String build() {
if (table == null) throw new IllegalStateException(
"FROM clause is required");
StringBuilder sql = new StringBuilder("SELECT ");
sql.append(String.join(", ", columns));
sql.append(" FROM ").append(table);
joins.forEach(j -> sql.append(" ").append(j));
if (!conditions.isEmpty()) {
sql.append(" WHERE ")
.append(String.join(" AND ", conditions));
}
if (orderBy != null) {
sql.append(" ORDER BY ")
.append(orderBy).append(" ").append(orderDirection);
}
if (limit != null) sql.append(" LIMIT ").append(limit);
if (offset != null) sql.append(" OFFSET ").append(offset);
return sql.toString();
}
}
// Kullanım — SQL gibi okunur
String query = QueryBuilder
.select("u.id", "u.name", "u.email", "o.total_amount")
.from("users u")
.join("orders o", "o.user_id = u.id")
.where("u.active = true")
.where("o.created_at > '2024-01-01'")
.orderBy("o.total_amount").desc()
.limit(50)
.offset(0)
.build();
// SELECT u.id, u.name, u.email, o.total_amount
// FROM users u
// JOIN orders o ON o.user_id = u.id
// WHERE u.active = true AND o.created_at > '2024-01-01'
// ORDER BY o.total_amount DESC
// LIMIT 50 OFFSET 0Step Builder — Zorunlu Adımları Garanti Etme
Normal builder'da zorunlu alanları atlamak mümkündür (build'de runtime hatası verir). Step builder, derleme zamanında zorunlu alanları garanti eder:
// Adım arayüzleri
public interface OrderBuilder {
WithCustomer orderId(String orderId);
interface WithCustomer {
WithAmount customerId(Long customerId);
}
interface WithAmount {
FinalStep totalAmount(BigDecimal amount);
}
interface FinalStep {
FinalStep express(boolean express);
FinalStep shippingMethod(String method);
FinalStep notes(String notes);
Order build();
}
}
// Implementasyon
public class Order {
private final String orderId;
private final Long customerId;
private final BigDecimal totalAmount;
private final boolean express;
private final String shippingMethod;
private final String notes;
private Order(StepBuilder builder) {
this.orderId = builder.orderId;
this.customerId = builder.customerId;
this.totalAmount = builder.totalAmount;
this.express = builder.express;
this.shippingMethod = builder.shippingMethod;
this.notes = builder.notes;
}
public static OrderBuilder.WithCustomer builder(String orderId) {
StepBuilder b = new StepBuilder();
b.orderId = orderId;
return b;
}
private static class StepBuilder implements
OrderBuilder.WithCustomer,
OrderBuilder.WithAmount,
OrderBuilder.FinalStep {
private String orderId;
private Long customerId;
private BigDecimal totalAmount;
private boolean express = false;
private String shippingMethod = "STANDARD";
private String notes;
@Override
public OrderBuilder.WithAmount customerId(Long id) {
this.customerId = id;
return this;
}
@Override
public OrderBuilder.FinalStep totalAmount(BigDecimal amount) {
this.totalAmount = amount;
return this;
}
@Override
public OrderBuilder.FinalStep express(boolean express) {
this.express = express;
return this;
}
@Override
public OrderBuilder.FinalStep shippingMethod(String method) {
this.shippingMethod = method;
return this;
}
@Override
public OrderBuilder.FinalStep notes(String notes) {
this.notes = notes;
return this;
}
@Override
public Order build() {
return new Order(this);
}
}
}
// Kullanım — zorunlu alanları atlamak DERLEME HATASI verir
Order order = Order.builder("ORD-001") // orderId zorunlu
.customerId(42L) // customerId zorunlu
.totalAmount(BigDecimal.valueOf(99)) // totalAmount zorunlu
.express(true) // opsiyonel
.build();
// Order.builder("ORD-001").build(); → DERLEME HATASI
// (customerId ve totalAmount eksik)Immutable Objects — Değiştirilemez Nesneler
Builder pattern'in en büyük avantajlarından biri immutable nesne oluşturmayı kolaylaştırmasıdır:
Neden Immutable?
// Mutable nesne — thread-safe DEĞİL
public class MutableOrder {
private String status;
public void setStatus(String status) { this.status = status; }
// Thread A: order.setStatus("SHIPPED")
// Thread B: order.setStatus("CANCELLED")
// Sonuç: belirsiz! Race condition.
}
// Immutable nesne — thread-safe
public class ImmutableOrder {
private final String status;
// Setter YOK — değiştirmek için yeni nesne oluştur
public ImmutableOrder withStatus(String newStatus) {
return new ImmutableOrder(/* tüm alanlar */, newStatus);
}
}Immutable nesnelerin avantajları:
Thread-safe: Ek senkronizasyon gerekmez
Yan etkiden uzak: Defensive copy gerekli değil
Hash-based collection'larda güvenli:
HashMap,HashSetkey olarak kullanılabilirDebugging kolaylığı: Nesne oluşturulduktan sonra değişmez, state takibi kolay
Spring Boot'ta Builder Kullanım Alanları
// 1. ResponseEntity builder (Spring'in kendi builder'ı)
return ResponseEntity
.status(HttpStatus.CREATED)
.header("Location", "/api/orders/" + id)
.header("X-Request-Id", requestId)
.body(orderResponse);
// 2. WebClient builder
WebClient client = WebClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Authorization", "Bearer " + token)
.filter(ExchangeFilterFunctions.basicAuthentication())
.build();
// 3. RestClient builder
RestClient restClient = RestClient.builder()
.baseUrl("http://user-service")
.defaultHeader("Content-Type", "application/json")
.requestInterceptor(new LoggingInterceptor())
.build();
// 4. Specification builder (JPA)
Specification<Order> spec = Specification
.where(OrderSpec.hasStatus(OrderStatus.PENDING))
.and(OrderSpec.createdAfter(startDate))
.and(OrderSpec.totalAmountGreaterThan(BigDecimal.valueOf(100)));Ne Zaman Builder Kullanmalı?
| Durum | Builder Gerekli mi? |
|---|---|
| 4+ parametre | ✅ Evet |
| Opsiyonel parametreler var | ✅ Evet |
| Immutable nesne gerekli | ✅ Evet |
| Test data oluşturma | ✅ Çok kullanışlı |
| 2-3 parametre, hepsi zorunlu | ❌ Constructor yeterli |
| Basit DTO (request/response) | ❌ Record yeterli |
Yaygın Hatalar
1. @Builder.Default Unutmak
// ❌ YANLIŞ — varsayılan değer Builder'da çalışmaz
@Builder
public class Order {
private String status = "PENDING"; // Builder bunu YOKSAYAR
}
// Order.builder().build() → status = null!
// ✅ DOĞRU
@Builder
public class Order {
@Builder.Default
private String status = "PENDING";
}2. Mutable Koleksiyon
// ❌ YANLIŞ — immutable olması gereken nesne aslında mutable
@Builder
public class Order {
private final List<String> tags; // tags listesi dışarıdan değiştirilebilir!
}
Order order = Order.builder().tags(myList).build();
myList.add("hacked"); // order.tags da değişir!
// ✅ DOĞRU — defensive copy
@Builder
public class Order {
private final List<String> tags;
// Constructor'da defensive copy
private Order(String orderId, List<String> tags) {
this.tags = tags != null
? Collections.unmodifiableList(new ArrayList<>(tags))
: List.of();
}
}Özet
Builder pattern, çok parametreli nesneleri okunabilir ve güvenli şekilde oluşturur
Lombok @Builder, boilerplate kodu ortadan kaldırır —
@Builder.Defaultile varsayılan değerlerFluent API, method chaining ile okunabilir, akıcı kod sağlar
Step Builder, zorunlu alanları derleme zamanında garanti eder
Immutable objects, thread-safety ve yan efki güvenliği sağlar
toBuilder(), mevcut nesneyi kopyalayıp değiştirmek için kullanışlıdır
Ne zaman: 4+ parametre, opsiyonel alanlar, immutability gerekli ise Builder kullanın
AI Asistan
Sorularını yanıtlamaya hazır