← Kursa Dön
📄 Text · 25 min

Design Patterns in Spring

Giriş

Design pattern'ler, yazılım tasarımında tekrar eden sorunlara kanıtlanmış çözümlerdir. Gang of Four (GoF) kitabında 23 pattern tanımlanmıştır, ancak pratikte her gün kullandığımız sayı çok daha azdır. İyi haber şu: Spring Framework'ün kendisi zaten birçok pattern üzerine inşa edilmiştir — IoC container (Factory), AOP (Proxy/Decorator), event sistemi (Observer), RestTemplate (Template Method).

Pattern'leri öğrenmek, "bu kodu nereye, nasıl yazmalıyım?" sorusuna cevap verir. Bir restoran düşünün: garsonun siparişi mutfağa iletme şekli (Observer), farklı şeflerin aynı yemek tarifini uygulama şekli (Strategy), yemek hazırlama adımlarının sırası (Template Method) — bunların hepsi pattern'dir.

Bu derste Spring Boot uygulamalarında en sık kullanılan beş tasarım kalıbını, gerçek dünya senaryolarıyla ve çalışan kod örnekleriyle derinlemesine inceleyeceğiz.

1. Factory Pattern — Nesne Oluşturmayı Soyutlama

Factory pattern, nesne oluşturma mantığını soyutlar. Client kodu hangi sınıfın instance'ının oluşturulacağını bilmez — Factory karar verir.

Neden Gerekli?

// ❌ KÖTÜ — yeni ödeme yöntemi eklemek için if-else değiştirmek gerekir
public PaymentResult processPayment(PaymentRequest request) {
    if (request.getMethod() == PaymentMethod.CREDIT_CARD) {
        // 50 satır kredi kartı mantığı
    } else if (request.getMethod() == PaymentMethod.BANK_TRANSFER) {
        // 50 satır havale mantığı
    } else if (request.getMethod() == PaymentMethod.DIGITAL_WALLET) {
        // 50 satır dijital cüzdan mantığı
    }
    // Yeni yöntem? Buraya bir else if daha ekle...
    // Open/Closed Principle ihlali!
}

Spring'le Factory Pattern

// 1. Ortak arayüz
public interface PaymentProcessor {
    PaymentResult process(PaymentRequest request);
    PaymentMethod getSupportedMethod();
}

// 2. Implementasyonlar — her biri bir @Component
@Component
@RequiredArgsConstructor
@Slf4j
public class CreditCardProcessor implements PaymentProcessor {

    private final PaymentGatewayClient gateway;

    @Override
    public PaymentResult process(PaymentRequest request) {
        log.info("Processing credit card payment: {}",
            request.getAmount());
        // Kredi kartı işleme mantığı
        GatewayResponse response = gateway.charge(
            request.getCardNumber(),
            request.getAmount(),
            request.getCurrency()
        );
        return new PaymentResult(
            response.isSuccess(),
            "CC-" + response.getTransactionId(),
            response.getMessage()
        );
    }

    @Override
    public PaymentMethod getSupportedMethod() {
        return PaymentMethod.CREDIT_CARD;
    }
}

@Component
public class BankTransferProcessor implements PaymentProcessor {

    @Override
    public PaymentResult process(PaymentRequest request) {
        // Havale işleme mantığı
        return new PaymentResult(true, "BT-" + UUID.randomUUID(),
            "Havale başarılı");
    }

    @Override
    public PaymentMethod getSupportedMethod() {
        return PaymentMethod.BANK_TRANSFER;
    }
}

@Component
public class DigitalWalletProcessor implements PaymentProcessor {

    @Override
    public PaymentResult process(PaymentRequest request) {
        // Dijital cüzdan mantığı
        return new PaymentResult(true, "DW-" + UUID.randomUUID(),
            "Dijital cüzdan ödemesi başarılı");
    }

    @Override
    public PaymentMethod getSupportedMethod() {
        return PaymentMethod.DIGITAL_WALLET;
    }
}

// 3. Factory — Spring IoC ile zarif çözüm
@Component
public class PaymentProcessorFactory {

    private final Map<PaymentMethod, PaymentProcessor> processors;

    // Spring, tüm PaymentProcessor bean'lerini otomatik inject eder
    public PaymentProcessorFactory(List<PaymentProcessor> processorList) {
        this.processors = processorList.stream()
            .collect(Collectors.toMap(
                PaymentProcessor::getSupportedMethod,
                Function.identity()
            ));
    }

    public PaymentProcessor getProcessor(PaymentMethod method) {
        PaymentProcessor processor = processors.get(method);
        if (processor == null) {
            throw new UnsupportedOperationException(
                "Desteklenmeyen ödeme yöntemi: " + method);
        }
        return processor;
    }

    public Set<PaymentMethod> getSupportedMethods() {
        return Collections.unmodifiableSet(processors.keySet());
    }
}

// 4. Kullanım — temiz ve genişletilebilir
@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentProcessorFactory factory;

    public PaymentResult pay(PaymentRequest request) {
        PaymentProcessor processor =
            factory.getProcessor(request.getMethod());
        return processor.process(request);
    }
}

Yeni bir ödeme yöntemi eklemek için: sadece yeni bir @Component sınıfı yazarsınız. Factory, Service veya Controller'da hiçbir değişiklik gerekmez. Bu, Open/Closed Principle (değişikliğe kapalı, genişletmeye açık) demektir.

💡 Spring'in Sihri: List<PaymentProcessor> inject edildiğinde, Spring IoC container'daki TÜM PaymentProcessor implementasyonlarını otomatik bulur ve listeye ekler. Factory'de new kelimesi yoktur.

2. Strategy Pattern — Çalışma Zamanında Algoritma Seçimi

Strategy pattern, bir algoritma ailesini tanımlar ve her birini değiştirilebilir kılar. Factory'ye benzer ancak odak algoritma/davranış üzerindedir.

Senaryo: Farklı İndirim Stratejileri

public interface DiscountStrategy {
    BigDecimal calculateDiscount(BigDecimal price, Customer customer);
    boolean supports(DiscountType type);
    int priority();  // Birden fazla strateji uygulanabilirse
}

@Component
public class PercentageDiscount implements DiscountStrategy {

    @Override
    public BigDecimal calculateDiscount(BigDecimal price,
                                        Customer customer) {
        BigDecimal rate = BigDecimal.valueOf(0.10); // %10
        return price.multiply(rate);
    }

    @Override
    public boolean supports(DiscountType type) {
        return type == DiscountType.PERCENTAGE;
    }

    @Override
    public int priority() { return 1; }
}

@Component
public class LoyaltyDiscount implements DiscountStrategy {

    @Override
    public BigDecimal calculateDiscount(BigDecimal price,
                                        Customer customer) {
        int years = customer.getMembershipYears();
        // Yıl başına %2, max %20
        double rate = Math.min(years * 0.02, 0.20);
        return price.multiply(BigDecimal.valueOf(rate));
    }

    @Override
    public boolean supports(DiscountType type) {
        return type == DiscountType.LOYALTY;
    }

    @Override
    public int priority() { return 2; }
}

@Component
public class VolumeDiscount implements DiscountStrategy {

    @Override
    public BigDecimal calculateDiscount(BigDecimal price,
                                        Customer customer) {
        // Toplam sipariş sayısına göre indirim
        long orderCount = customer.getTotalOrders();
        if (orderCount > 100) return price.multiply(BigDecimal.valueOf(0.15));
        if (orderCount > 50) return price.multiply(BigDecimal.valueOf(0.10));
        if (orderCount > 20) return price.multiply(BigDecimal.valueOf(0.05));
        return BigDecimal.ZERO;
    }

    @Override
    public boolean supports(DiscountType type) {
        return type == DiscountType.VOLUME;
    }

    @Override
    public int priority() { return 3; }
}

// Strategy seçici servisi
@Service
@RequiredArgsConstructor
public class PricingService {

    private final List<DiscountStrategy> strategies;

    public BigDecimal calculateFinalPrice(BigDecimal price,
                                          Customer customer,
                                          DiscountType type) {
        BigDecimal discount = strategies.stream()
            .filter(s -> s.supports(type))
            .findFirst()
            .map(s -> s.calculateDiscount(price, customer))
            .orElse(BigDecimal.ZERO);

        return price.subtract(discount).max(BigDecimal.ZERO);
    }

    // En iyi indirimi otomatik seç
    public BigDecimal calculateBestDiscount(BigDecimal price,
                                            Customer customer) {
        return strategies.stream()
            .map(s -> s.calculateDiscount(price, customer))
            .max(BigDecimal::compareTo)
            .orElse(BigDecimal.ZERO);
    }
}

Factory vs Strategy — Ne Zaman Hangisi?

KriterFactoryStrategy
OdakNesne oluşturmaAlgoritma/davranış seçimi
DöndürenNesne instance'ıHesaplama sonucu
Kullanım"Hangi processor?""Hangi algoritma?"
ÖrnekPaymentProcessorDiscountStrategy

3. Template Method — Ortak İş Akışları

Template Method, bir algoritmanın iskeletini üst sınıfta tanımlar; alt sınıflar belirli adımları override eder. "Aynı dans, farklı adımlar" prensibidir.

Senaryo: Farklı Veri Kaynakları İçin Import Akışı

@Slf4j
public abstract class DataImportTemplate<T> {

    // Template method — iskelet (final: alt sınıf değiştiremez)
    public final ImportResult importData(InputStream source) {
        String importName = getImportName();
        log.info("Starting import: {}", importName);

        try {
            // Adım 1: Oku
            List<T> rawData = readData(source);
            log.info("Read {} records", rawData.size());

            // Adım 2: Doğrula
            List<T> validated = validate(rawData);
            log.info("Validated {} / {} records",
                validated.size(), rawData.size());

            // Adım 3: Dönüştür (hook — opsiyonel override)
            List<T> transformed = transform(validated);

            // Adım 4: Kaydet
            int saved = save(transformed);
            log.info("Saved {} records", saved);

            // Adım 5: Post-processing (hook)
            onSuccess(saved);

            return new ImportResult(
                rawData.size(), validated.size(), saved, null);

        } catch (Exception e) {
            log.error("Import failed: {}", importName, e);
            onFailure(e);
            return new ImportResult(0, 0, 0, e.getMessage());
        }
    }

    // Soyut metotlar — ALT SINIF ZORUNLU implement eder
    protected abstract String getImportName();
    protected abstract List<T> readData(InputStream source);
    protected abstract List<T> validate(List<T> data);
    protected abstract int save(List<T> data);

    // Hook metotlar — ALT SINIF İSTERSE override eder
    protected List<T> transform(List<T> data) {
        return data; // Varsayılan: dönüşüm yok
    }

    protected void onSuccess(int savedCount) {
        // Varsayılan: hiçbir şey yapma
    }

    protected void onFailure(Exception e) {
        // Varsayılan: hiçbir şey yapma
    }
}

// CSV Product Importer
@Service
@RequiredArgsConstructor
public class CsvProductImporter extends DataImportTemplate<Product> {

    private final ProductRepository productRepository;
    private final CsvParser csvParser;
    private final ApplicationEventPublisher eventPublisher;

    @Override
    protected String getImportName() {
        return "CSV Product Import";
    }

    @Override
    protected List<Product> readData(InputStream source) {
        return csvParser.parse(source, Product.class);
    }

    @Override
    protected List<Product> validate(List<Product> data) {
        return data.stream()
            .filter(p -> p.getName() != null && !p.getName().isBlank())
            .filter(p -> p.getPrice() != null && p.getPrice().signum() > 0)
            .filter(p -> p.getSku() != null)
            .toList();
    }

    @Override
    protected List<Product> transform(List<Product> data) {
        // SKU'ları büyük harfe çevir
        data.forEach(p -> p.setSku(p.getSku().toUpperCase()));
        return data;
    }

    @Override
    protected int save(List<Product> data) {
        return productRepository.saveAll(data).size();
    }

    @Override
    protected void onSuccess(int savedCount) {
        eventPublisher.publishEvent(
            new ProductImportCompleted(savedCount));
    }
}

// Excel User Importer
@Service
@RequiredArgsConstructor
public class ExcelUserImporter extends DataImportTemplate<User> {

    private final UserRepository userRepository;
    private final ExcelParser excelParser;

    @Override
    protected String getImportName() {
        return "Excel User Import";
    }

    @Override
    protected List<User> readData(InputStream source) {
        return excelParser.parse(source, User.class);
    }

    @Override
    protected List<User> validate(List<User> data) {
        return data.stream()
            .filter(u -> u.getEmail() != null && u.getEmail().contains("@"))
            .filter(u -> !userRepository.existsByEmail(u.getEmail()))
            .toList();
    }

    @Override
    protected int save(List<User> data) {
        return userRepository.saveAll(data).size();
    }
}

4. Observer Pattern — Event Sistemi

Observer pattern, nesneler arasında one-to-many bağımlılık kurar. Bir nesne durum değiştirdiğinde, bağımlı tüm nesneler otomatik bilgilendirilir. Spring'in ApplicationEventPublisher mekanizması bu pattern'in doğrudan uygulamasıdır.

Neden Event Kullanmalı?

// ❌ KÖTÜ — tight coupling, her yeni ihtiyaçta metod değişir
@Service
public class OrderService {

    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        Order order = orderRepository.save(mapToEntity(request));

        // Her yeni ihtiyaç için buraya kod eklenmeli
        emailService.sendOrderConfirmation(order);    // +1 dependency
        inventoryService.reserveStock(order);           // +2 dependency
        analyticsService.trackPurchase(order);          // +3 dependency
        loyaltyService.addPoints(order);                // +4 dependency
        notificationService.sendPush(order);            // +5 dependency
        // OrderService artık 5+ servise bağımlı — test etmek kabus

        return order;
    }
}
// ✅ DOĞRU — loose coupling, event-driven
// Event tanımı
public record OrderCreatedEvent(
    Long orderId,
    Long customerId,
    BigDecimal amount,
    List<Long> productIds,
    Instant createdAt
) {}

// Publisher (Subject)
@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        Order order = orderRepository.save(mapToEntity(request));

        // Tek satır — kim dinlerse dinlesin
        eventPublisher.publishEvent(new OrderCreatedEvent(
            order.getId(),
            order.getCustomerId(),
            order.getTotalAmount(),
            order.getProductIds(),
            Instant.now()
        ));

        return order;
    }
}

// Observer'lar (Listeners) — birbirinden ve OrderService'ten bağımsız
@Component
@RequiredArgsConstructor
@Slf4j
public class OrderEmailListener {

    private final EmailService emailService;

    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        log.info("Sending order confirmation email | orderId={}",
            event.orderId());
        emailService.sendOrderConfirmation(
            event.customerId(), event.orderId());
    }
}

@Component
@RequiredArgsConstructor
public class InventoryListener {

    private final InventoryService inventoryService;

    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        inventoryService.reserveStock(
            event.productIds(), event.orderId());
    }
}

@Component
@RequiredArgsConstructor
public class AnalyticsListener {

    private final AnalyticsService analyticsService;

    @Async  // Ayrı thread'de — ana akışı yavaşlatmaz
    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        analyticsService.trackPurchase(
            event.customerId(), event.amount());
    }
}

@Component
public class LoyaltyListener {

    @Async
    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        // Sadakat puanı ekle
        int points = event.amount()
            .multiply(BigDecimal.valueOf(10))
            .intValue();
        loyaltyService.addPoints(event.customerId(), points);
    }
}

@TransactionalEventListener

Transaction commit olduktan SONRA event'i işlemek için:

@Component
public class OrderNotificationListener {

    // Transaction commit olduktan SONRA çalışır
    // → Eğer sipariş kaydı başarısızsa email GÖNDERİLMEZ
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onOrderCreated(OrderCreatedEvent event) {
        emailService.sendConfirmation(event.orderId());
    }

    // Transaction rollback olursa çalışır
    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void onOrderFailed(OrderCreatedEvent event) {
        alertService.notifyOrderFailure(event.orderId());
    }
}

⚠️ Dikkat: @EventListener default olarak aynı thread ve transaction içinde çalışır. Listener'da hata olursa ana transaction da rollback olur. Bunu istemiyorsanız @Async kullanın veya @TransactionalEventListener ile faz belirtin.

5. Decorator Pattern — Cross-Cutting Concerns

Decorator pattern, bir nesnenin davranışını sarmalayarak (wrapping) genişletir. Spring AOP bunu transparan şekilde uygular — orijinal koda dokunmadan loglama, caching, retry, metrikleme ekleyebilirsiniz.

Loglama Aspect'i

@Aspect
@Component
@Slf4j
public class PerformanceLoggingAspect {

    @Around("execution(* com.example..service.*.*(..))")
    public Object logPerformance(ProceedingJoinPoint joinPoint)
            throws Throwable {
        String method = joinPoint.getSignature().toShortString();
        long start = System.currentTimeMillis();

        try {
            Object result = joinPoint.proceed();
            long elapsed = System.currentTimeMillis() - start;

            if (elapsed > 1000) {
                log.warn("SLOW METHOD: {} took {}ms", method, elapsed);
            } else {
                log.debug("{} completed in {}ms", method, elapsed);
            }

            return result;
        } catch (Exception e) {
            long elapsed = System.currentTimeMillis() - start;
            log.error("{} failed after {}ms: {}",
                method, elapsed, e.getMessage());
            throw e;
        }
    }
}

Retry Aspect'i

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retryable {
    int maxRetries() default 3;
    long delayMs() default 1000;
    Class<? extends Exception>[] retryOn() default {Exception.class};
}

@Aspect
@Component
@Slf4j
public class RetryAspect {

    @Around("@annotation(retryable)")
    public Object retry(ProceedingJoinPoint joinPoint,
                        Retryable retryable) throws Throwable {
        int maxRetries = retryable.maxRetries();
        String method = joinPoint.getSignature().toShortString();

        for (int attempt = 1; attempt <= maxRetries; attempt++) {
            try {
                return joinPoint.proceed();
            } catch (Exception e) {
                boolean shouldRetry = Arrays.stream(retryable.retryOn())
                    .anyMatch(type -> type.isInstance(e));

                if (!shouldRetry || attempt == maxRetries) {
                    log.error("All {} retries failed for {}",
                        maxRetries, method);
                    throw e;
                }

                long delay = retryable.delayMs() * attempt; // exponential
                log.warn("Retry {}/{} for {} after {}ms",
                    attempt, maxRetries, method, delay);
                Thread.sleep(delay);
            }
        }
        throw new IllegalStateException("Unreachable");
    }
}

// Kullanım
@Service
public class ExternalApiService {

    @Retryable(maxRetries = 3, delayMs = 500,
               retryOn = {IOException.class, TimeoutException.class})
    public ExternalData fetchData(String query) {
        return externalClient.search(query);
    }
}

Auditing Aspect'i

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {
    String action() default "";
}

@Aspect
@Component
@Slf4j
public class AuditAspect {

    @Around("@annotation(audited)")
    public Object audit(ProceedingJoinPoint joinPoint,
                        Audited audited) throws Throwable {
        Authentication auth = SecurityContextHolder.getContext()
            .getAuthentication();
        String user = auth != null ? auth.getName() : "system";
        String action = audited.action().isEmpty()
            ? joinPoint.getSignature().toShortString()
            : audited.action();

        log.info("AUDIT | user={} | action={} | args={}",
            user, action,
            Arrays.toString(joinPoint.getArgs()));

        Object result = joinPoint.proceed();

        log.info("AUDIT | user={} | action={} | result=SUCCESS",
            user, action);

        return result;
    }
}

// Kullanım
@Service
public class UserService {

    @Audited(action = "DELETE_USER")
    public void deleteUser(Long userId) { ... }

    @Audited(action = "CHANGE_PASSWORD")
    public void changePassword(Long userId, String newPass) { ... }
}

Pattern'leri Birleştirmek — Gerçek Dünya Örneği

Gerçek projelerde pattern'ler birlikte kullanılır:

// Factory + Strategy: Bildirim gönderimi
public interface NotificationSender {
    void send(Notification notification);
    NotificationChannel getChannel();
}

@Component
public class EmailSender implements NotificationSender { ... }

@Component
public class SmsSender implements NotificationSender { ... }

@Component
public class PushSender implements NotificationSender { ... }

@Component
public class NotificationFactory {
    private final Map<NotificationChannel, NotificationSender> senders;
    // Factory aynı pattern — Spring IoC ile inject
}

// Observer: Event tetikleme
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
    NotificationSender sender = factory.getSender(
        customer.getPreferredChannel());  // Factory kullanımı
    sender.send(orderNotification);        // Strategy kullanımı
}

// Template Method: Bildirim gönderme akışı
public abstract class AbstractNotificationSender
        implements NotificationSender {

    public final void send(Notification notification) {
        validate(notification);           // Abstract
        String formatted = format(notification);  // Abstract
        doSend(formatted, notification.getRecipient());  // Abstract
        log(notification);                // Concrete
    }
}

Yaygın Hatalar

  1. Over-engineering: Her yere pattern uygulamak. 2-3 implementasyon yoksa Factory/Strategy gerekmez

  2. Yanlış pattern seçimi: Factory gereken yerde Strategy, Strategy gereken yerde if-else

  3. Aspect sıralama: Birden fazla Aspect varsa @Order ile sıralama belirtin

  4. Event storm: Her şeyi event yaparak sistemi anlaşılmaz kılmak

Özet

  • Factory Pattern: Spring IoC ile List<Interface> inject → otomatik Factory. Yeni implementasyon = yeni @Component, sıfır değişiklik

  • Strategy Pattern: Çalışma zamanında algoritma seçimi. supports() metodu ile uygun stratejiyi bul

  • Template Method: Ortak iş akışını abstract class'ta tanımla, değişen adımları alt sınıflara bırak

  • Observer Pattern: ApplicationEventPublisher + @EventListener — loose coupling, async desteği

  • Decorator Pattern: Spring AOP ile cross-cutting concerns — loglama, retry, auditing, metrikleme

  • Birlikte kullanım: Pattern'ler izole değildir — Factory + Strategy + Observer birlikte güçlüdür