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ÜMPaymentProcessorimplementasyonlarını otomatik bulur ve listeye ekler. Factory'denewkelimesi 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?
| Kriter | Factory | Strategy |
|---|---|---|
| Odak | Nesne oluşturma | Algoritma/davranış seçimi |
| Döndüren | Nesne instance'ı | Hesaplama sonucu |
| Kullanım | "Hangi processor?" | "Hangi algoritma?" |
| Örnek | PaymentProcessor | DiscountStrategy |
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:
@EventListenerdefault olarak aynı thread ve transaction içinde çalışır. Listener'da hata olursa ana transaction da rollback olur. Bunu istemiyorsanız@Asynckullanın veya@TransactionalEventListenerile 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
Over-engineering: Her yere pattern uygulamak. 2-3 implementasyon yoksa Factory/Strategy gerekmez
Yanlış pattern seçimi: Factory gereken yerde Strategy, Strategy gereken yerde if-else
Aspect sıralama: Birden fazla Aspect varsa
@Orderile sıralama belirtinEvent 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şiklikStrategy Pattern: Çalışma zamanında algoritma seçimi.
supports()metodu ile uygun stratejiyi bulTemplate Method: Ortak iş akışını
abstract class'ta tanımla, değişen adımları alt sınıflara bırakObserver Pattern:
ApplicationEventPublisher+@EventListener— loose coupling, async desteğiDecorator 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
AI Asistan
Sorularını yanıtlamaya hazır