← Kursa Dön
📄 Text · 20 min

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 daha

Alternatif: 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 0

Step 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, HashSet key olarak kullanılabilir

  • Debugging 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ı?

DurumBuilder 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.Default ile varsayılan değerler

  • Fluent 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