Mikroservis Mimarisi İçin Bilinmesi Gereken 25 Tasarım Deseni

15 Haziran 2026 · netologist · 18 dakika, 3629 kelime ·

Java ile Pratik Bir Rehber (Resilience4j, Spring Cloud, Kafka ve Daha Fazlası)

Bu rehber, mikroservislerde en çok kullanılan 25 tasarım desenini adım adım anlatır. Her bölümde: desenin hangi problemi çözdüğü, ne zaman kullanılacağı, bir Java kod örneği ve gerçek projelerde sıkça kullanılan araçlar/kütüphaneler (Resilience4j, Spring Cloud, Kafka, Eureka vb.) yer alır.


İçindekiler

  1. Circuit Breaker (Devre Kesici)
  2. Retry (Yeniden Deneme)
  3. Bulkhead (Bölmeleme)
  4. Saga Deseni
  5. API Gateway
  6. Service Discovery (Servis Keşfi)
  7. Servis Başına Veritabanı
  8. Olay Güdümlü Mimari (Event-Driven Architecture)
  9. CQRS Deseni
  10. Event Sourcing (Olay Kaynaklı Mimari)
  11. Strangler Fig Deseni
  12. Sidecar Deseni
  13. Ambassador Deseni
  14. Adapter Deseni
  15. Proxy Deseni
  16. Factory Deseni
  17. Strategy Deseni
  18. Observer Deseni
  19. Singleton Deseni
  20. Builder Deseni
  21. Decorator Deseni
  22. Repository Deseni
  23. Dependency Injection Deseni
  24. Outbox Deseni
  25. Idempotency (Eş Güçlülük) Deseni

1. Circuit Breaker (Devre Kesici)

Çözdüğü problem: Zaten hata veren bir servise istek göndermeyi durdurarak zincirleme hataları önler; servise toparlanması için zaman tanır, üzerine yük bindirmez.

Ne zaman kullanılır: A servisi ağ üzerinden B servisini çağırdığında ve B yavaşlayabilir veya erişilemez hale gelebiliyorsa.

Yaygın araç: Resilience4j (eski adıyla Hystrix, artık kullanılmıyor).

// build.gradle: implementation 'io.github.resilience4j:resilience4j-spring-boot3'

@Service
public class InventoryClient {

    private final RestTemplate restTemplate;

    public InventoryClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @CircuitBreaker(name = "inventoryService", fallbackMethod = "fallbackStock")
    public StockResponse checkStock(String productId) {
        return restTemplate.getForObject(
            "http://inventory-service/stock/" + productId, StockResponse.class);
    }

    // Fallback imzası eşleşmeli + ekstra Throwable parametresi almalı
    private StockResponse fallbackStock(String productId, Throwable t) {
        return new StockResponse(productId, 0, "UNAVAILABLE");
    }
}
# application.yml
resilience4j.circuitbreaker:
  instances:
    inventoryService:
      sliding-window-size: 10
      failure-rate-threshold: 50
      wait-duration-in-open-state: 5s
      permitted-number-of-calls-in-half-open-state: 3

Son 10 çağrıda hata oranı %50’yi geçtiğinde devre “açılır” ve çağrılar 5 saniye boyunca hızlıca başarısız olur (fallback devreye girer); ardından birkaç deneme çağrısına izin verilir (yarı-açık durum).


2. Retry (Yeniden Deneme)

Çözdüğü problem: Geçici bir sorun (kısa süreli ağ kesintisi, anlık aşırı yük) nedeniyle başarısız olan bir isteği, vazgeçmeden önce otomatik olarak yeniden dener.

Ne zaman kullanılır: Idempotent (tekrar edilebilir) işlemlerde (GET veya tekrar edilmesi güvenli POST/PUT), hatanın geçici olma ihtimali yüksekse.

Yaygın araç: Resilience4j Retry, Spring Retry.

@Retry(name = "paymentService", fallbackMethod = "fallbackCharge")
public ChargeResponse chargeCard(ChargeRequest request) {
    return paymentClient.charge(request);
}

private ChargeResponse fallbackCharge(ChargeRequest request, Throwable t) {
    return ChargeResponse.failed("Ödeme servisi yeniden denemelerden sonra hala erişilemez durumda");
}
resilience4j.retry:
  instances:
    paymentService:
      max-attempts: 3
      wait-duration: 500ms
      retry-exceptions:
        - java.io.IOException
        - org.springframework.web.client.ResourceAccessException

İpucu: Retry ile Circuit Breaker’ı birlikte kullanın - devre açıldığında Retry hızlıca vazgeçmeli ki zaten çökmüş bir servise gereksiz yük binmesin.


3. Bulkhead (Bölmeleme)

Çözdüğü problem: Her bağımlı servis için kaynakları (thread havuzları, bağlantı havuzları) izole eder; böylece yavaş/başarısız bir servis diğerlerinin ihtiyaç duyduğu kaynakları tüketemez.

Ne zaman kullanılır: Bir servis birden fazla alt bağımlılığı çağırdığında ve birinin diğerlerini aç bırakmasını istemediğinizde.

Yaygın araç: Resilience4j Bulkhead (semaphore veya thread-pool tabanlı).

@Bulkhead(name = "reportingService", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<Report> generateReport(String orgId) {
    return CompletableFuture.supplyAsync(() -> reportingClient.generate(orgId));
}
resilience4j.thread-pool-bulkhead:
  instances:
    reportingService:
      max-thread-pool-size: 10
      core-thread-pool-size: 5
      queue-capacity: 20

Bu yapılandırma, raporlama servisine yapılan çağrıları özel bir 10 thread’lik havuza sınırlar; böylece yavaş bir raporlama backend’i, örneğin checkout akışının ihtiyaç duyduğu thread’leri asla aç bırakamaz.


4. Saga Deseni

Çözdüğü problem: Birden fazla servise yayılan dağıtık işlemleri, her biri kendi telafi edici (compensating) aksiyonuna sahip yerel işlemler dizisi kullanarak yönetir.

Ne zaman kullanılır: Birden fazla servise yayılan çok adımlı iş süreçlerinde (ör. Sipariş → Ödeme → Envanter → Kargo), 2PC tarzı dağıtık işlem uygulanamaz olduğunda.

Yaygın araçlar: Axon Framework, Camunda, Spring State Machine veya Kafka olaylarıyla (koreografi) ya da bir orkestratör servisiyle elle kodlanmış çözümler.

// Orkestrasyon tabanlı saga - basit bir durum makinesi ile
public class OrderSagaOrchestrator {

    public void startSaga(Order order) {
        try {
            paymentService.charge(order.getPaymentInfo());
            inventoryService.reserve(order.getItems());
            shippingService.schedule(order);
            orderService.markCompleted(order.getId());
        } catch (PaymentException e) {
            orderService.markFailed(order.getId(), "Ödeme başarısız");
        } catch (InventoryException e) {
            paymentService.refund(order.getPaymentInfo()); // telafi edici aksiyon
            orderService.markFailed(order.getId(), "Envanter yetersiz");
        } catch (ShippingException e) {
            inventoryService.release(order.getItems());     // telafi edici aksiyon
            paymentService.refund(order.getPaymentInfo());  // telafi edici aksiyon
            orderService.markFailed(order.getId(), "Kargo başarısız");
        }
    }
}

Koreografi tabanlı bir saga’da her servis Kafka’ya bir domain olayı yayınlar (OrderCreated, PaymentCharged, InventoryReserved); sıradaki servis buna abone olur ve tepki verir - hata durumunda her adım ayrıca telafi edici bir olay da yayınlar (PaymentRefunded gibi).


5. API Gateway

Çözdüğü problem: İstemciler için tek bir giriş noktası sağlar; routing, kimlik doğrulama, rate limiting, SSL sonlandırma ve önbellekleme gibi işleri üstlenir, böylece her servis bunları ayrı ayrı uygulamak zorunda kalmaz.

Ne zaman kullanılır: Birden fazla istemci türü (mobil, web, partner) birden fazla backend servisine eriştiğinde.

Yaygın araç: Spring Cloud Gateway.

@Configuration
public class GatewayRoutesConfig {

    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("order-service", r -> r.path("/api/orders/**")
                .filters(f -> f
                    .circuitBreaker(c -> c.setName("orderServiceCB")
                        .setFallbackUri("forward:/fallback/orders"))
                    .requestRateLimiter(rl -> rl.setRateLimiter(redisRateLimiter())))
                .uri("lb://ORDER-SERVICE"))
            .route("payment-service", r -> r.path("/api/payments/**")
                .uri("lb://PAYMENT-SERVICE"))
            .build();
    }

    @Bean
    public RedisRateLimiter redisRateLimiter() {
        return new RedisRateLimiter(10, 20); // saniyede 10 istek, patlama (burst) 20
    }
}

Gateway genellikle isteği Eureka’da kayıtlı lb:// (load-balanced) backend servislerine yönlendirmeden önce, global bir filtrede JWT doğrulamasını da üstlenir.


6. Service Discovery (Servis Keşfi)

Çözdüğü problem: Servislerin birbirini dinamik olarak kaydettirmesini ve keşfetmesini sağlar; sabit kodlanmış host adı/port kullanımını önler.

Ne zaman kullanılır: Servis örneklerinin ölçeklenip küçüldüğü veya taşındığı her ortamda (konteynerler, bulutta otomatik ölçekleme).

Yaygın araç: Netflix Eureka (Spring Cloud Netflix), Consul.

// Order Service - kendini kaydeder
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}
// Payment Service - Order Service'i sabit IP yerine mantıksal isimle keşfeder
@Service
public class OrderClient {

    private final RestTemplate restTemplate;

    public OrderClient(@LoadBalanced RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public Order getOrder(String id) {
        return restTemplate.getForObject("http://ORDER-SERVICE/orders/" + id, Order.class);
    }
}
eureka:
  client:
    service-url:
      defaultZone: http://eureka-server:8761/eureka

7. Servis Başına Veritabanı

Çözdüğü problem: Her mikroservis kendi veritabanı şemasına/örneğine sahip olur; bu, bağımsızlığı artırır ve her takımın kendi ölçekleme ihtiyaçlarına göre doğru depolama teknolojisini seçmesini sağlar.

Ne zaman kullanılır: Düzgün bir şekilde ayrıştırılmış her mikroservis mimarisinde standart uygulama - tek bir paylaşılan veritabanından kaçının.

// Order Service - PostgreSQL
@Entity
@Table(name = "orders")
public class Order {
    @Id @GeneratedValue
    private Long id;
    private String customerId;
    private BigDecimal totalAmount;
    // Order servisi asla Payment veya Inventory veritabanını doğrudan sorgulamaz
}
// order-service için application.yml
spring:
  datasource:
    url: jdbc:postgresql://order-db:5432/orders_db

// inventory-service için application.yml (tamamen farklı bir teknoloji)
spring:
  data:
    mongodb:
      uri: mongodb://inventory-db:27017/inventory_db

Servisler arası veri ihtiyaçları, API çağrıları veya asenkron olaylar (aşağıdaki Olay Güdümlü Mimari’ye bakın) ile karşılanır - servis sınırları arasında asla doğrudan SQL join kullanılmaz.


8. Olay Güdümlü Mimari (Event-Driven Architecture)

Çözdüğü problem: Servisler, doğrudan senkron çağrılar yerine olaylar üzerinden asenkron iletişim kurar; bu da gevşek bağlılık ve daha iyi ölçeklenebilirlik sağlar.

Ne zaman kullanılır: Bir servisin, sıkı bağlılık veya bloklayıcı çağrı olmadan başka servislerdeki durum değişikliklerine tepki vermesi gerektiğinde.

Yaygın araç: Apache Kafka, RabbitMQ.

// Order Service - bir olay yayınlar
@Service
public class OrderService {

    private final KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate;

    public void createOrder(Order order) {
        orderRepository.save(order);
        OrderCreatedEvent event = new OrderCreatedEvent(order.getId(), order.getItems());
        kafkaTemplate.send("order-events", order.getId().toString(), event);
    }
}
// Payment Service - olayı dinler
@Component
public class OrderCreatedListener {

    @KafkaListener(topics = "order-events", groupId = "payment-service")
    public void handleOrderCreated(OrderCreatedEvent event) {
        paymentService.processPayment(event.getOrderId(), event.getItems());
    }
}
// Inventory Service ve Notification Service AYNI konuya bağımsız olarak abone olur
@KafkaListener(topics = "order-events", groupId = "inventory-service")
public void reserveStock(OrderCreatedEvent event) { /* ... */ }

9. CQRS Deseni (Command Query Responsibility Segregation)

Çözdüğü problem: Yazma modelini (komutlar) okuma modelinden (sorgular) ayırır; böylece her biri bağımsız olarak optimize edilip ölçeklenebilir.

Ne zaman kullanılır: Yazma modelinin normalize edilmiş yapısının okuma için ideal olmadığı, okumanın yoğun ve sorguların karmaşık olduğu sistemlerde (dashboard’lar, raporlama).

// YAZMA tarafı - komut modeli
@Service
public class CreateOrderCommandHandler {

    private final OrderWriteRepository writeRepository; // normalize edilmiş yazma veritabanı

    public void handle(CreateOrderCommand command) {
        Order order = new Order(command.getCustomerId(), command.getItems());
        writeRepository.save(order);
        eventPublisher.publish(new OrderCreatedEvent(order));
    }
}
// OKUMA tarafı - ayrı, denormalize edilmiş sorgu modeli, asenkron olarak güncellenir
@Component
public class OrderReadModelUpdater {

    @KafkaListener(topics = "order-events")
    public void on(OrderCreatedEvent event) {
        OrderSummaryView view = new OrderSummaryView(
            event.getOrderId(), event.getCustomerName(), event.getTotal());
        readRepository.save(view); // ör. Elasticsearch veya bir read-replica tablo
    }
}

@RestController
public class OrderQueryController {
    @GetMapping("/orders/{id}/summary")
    public OrderSummaryView getSummary(@PathVariable String id) {
        return readRepository.findById(id); // hızlı, denormalize okuma
    }
}

10. Event Sourcing (Olay Kaynaklı Mimari)

Çözdüğü problem: Mevcut durumu saklamak yerine, olayların tam sırasını saklar; gerektiğinde olaylar tekrar oynatılarak mevcut durum yeniden oluşturulur.

Ne zaman kullanılır: Tam denetim geçmişi, zamansal sorgular (“T anında durum neydi”) veya karmaşık geri alma/yeniden oynatma gereksinimleri olan alanlarda (bankacılık defterleri, sipariş yaşam döngüleri).

Yaygın araç: Axon Framework, EventStoreDB veya özel bir olay tablosu.

public class Account {
    private String accountId;
    private BigDecimal balance = BigDecimal.ZERO;
    private final List<Object> uncommittedEvents = new ArrayList<>();

    public void deposit(BigDecimal amount) {
        apply(new MoneyDepositedEvent(accountId, amount));
    }

    public void withdraw(BigDecimal amount) {
        if (balance.compareTo(amount) < 0) throw new InsufficientFundsException();
        apply(new MoneyWithdrawnEvent(accountId, amount));
    }

    private void apply(Object event) {
        mutate(event);
        uncommittedEvents.add(event);
    }

    private void mutate(Object event) {
        if (event instanceof MoneyDepositedEvent e) balance = balance.add(e.amount());
        if (event instanceof MoneyWithdrawnEvent e) balance = balance.subtract(e.amount());
    }

    // Durumu geçmişten yeniden oluştur
    public static Account rehydrate(String id, List<Object> history) {
        Account account = new Account();
        account.accountId = id;
        history.forEach(account::mutate);
        return account;
    }
}

EventStore, MoneyDepositedEvent/MoneyWithdrawnEvent kayıtlarını saklar; güncel bakiye bunları tekrar oynatarak her zaman türetilebilir ve üstelik tam bir denetim izini de bedava elde edersiniz.


11. Strangler Fig Deseni

Çözdüğü problem: Eski bir monolitik yapının parçalarını kademeli olarak mikroservislerle değiştirir; trafiği aşamalı olarak yeni servislere yönlendirerek monolit tamamen emekliye ayrılana kadar devam eder.

Ne zaman kullanılır: Riskli bir “büyük patlama” (big-bang) yeniden yazımı olmadan bir monoliti mikroservislere taşırken.

// Bir cephe/gateway, hangi özelliğin taşındığına göre yönlendirme yapar
@RestController
public class StranglerFacadeController {

    private final NewOrderServiceClient newOrderService;
    private final LegacyMonolithClient legacyMonolith;

    @GetMapping("/orders/{id}")
    public ResponseEntity<Order> getOrder(@PathVariable String id) {
        if (featureFlags.isEnabled("orders-migrated-to-microservice")) {
            return ResponseEntity.ok(newOrderService.getOrder(id));
        }
        return ResponseEntity.ok(legacyMonolith.getOrder(id));
    }
}

Zamanla, daha fazla endpoint legacyMonolith‘ten newOrderService‘e çevrilir; cephe trafiğin %100’ünü mikroservislere yönlendirene ve monolit devre dışı bırakılana kadar bu süreç devam eder.


12. Sidecar Deseni

Çözdüğü problem: Yardımcı bir bileşeni (loglama, güvenlik, izleme, service mesh proxy’si) bir servisin yanına, kendi süreç/konteynerinde konuşlandırır; böylece ana servisin kodu değiştirilmeden işlevsellik genişletilir.

Ne zaman kullanılır: Gözlemlenebilirlik, mTLS veya trafik yönetimi gibi çapraz kesen (cross-cutting) konularda - en yaygın olarak bir service mesh üzerinden uygulanır.

Yaygın araç: Envoy proxy (Istio), Kubernetes sidecar konteynerleri.

# Uygulama + sidecar içeren Kubernetes pod'u
apiVersion: v1
kind: Pod
metadata:
  name: order-service-pod
spec:
  containers:
    - name: order-service
      image: order-service:1.0
      ports:
        - containerPort: 8080
    - name: envoy-sidecar
      image: envoyproxy/envoy:v1.28
      ports:
        - containerPort: 9901 # admin/metrikler
// Java uygulamasının mTLS, yeniden deneme veya izleme (tracing) başlıklarından
// haberdar olmasına gerek yok - sidecar (Envoy) trafiği şeffaf şekilde yakalar.
@RestController
public class OrderController {
    @GetMapping("/orders/{id}")
    public Order getOrder(@PathVariable String id) {
        return orderService.findById(id); // sadece iş mantığı
    }
}

13. Ambassador Deseni

Çözdüğü problem: Bir proxy, giden çağrılar için yeniden deneme, devre kesme veya protokol dönüşümü gibi çapraz kesen konuları servis adına yönetir - sidecar’a benzer ama giden (outbound) trafiğe odaklanır.

Ne zaman kullanılır: İstemci tarafı ağ mantığını (retry, TLS, izleme) uygulama kodundan ayrı, yerel bir proxy sürecine taşımak istediğinizde.

// Uygulama kodu, gerçek dış servislerden habersiz, YEREL bir ambassador proxy'sini çağırır
@Service
public class ExternalServiceClient {

    private final RestTemplate restTemplate;

    public ServiceAResponse callServiceA(Request req) {
        // localhost'taki ambassador'ı çağırır, o da gerçek "External Service 1"e yönlendirir
        // ve yolda retry/circuit-breaking/loglama uygular
        return restTemplate.postForObject("http://localhost:9000/service-a", req, ServiceAResponse.class);
    }

    public ServiceBResponse callServiceB(Request req) {
        return restTemplate.postForObject("http://localhost:9001/service-b", req, ServiceBResponse.class);
    }
}

Ambassador (genellikle sidecar konteyner olarak çalışan küçük bir Envoy veya özel bir Go/Java proxy’si), onu kullanan her servis için TLS, retry ve devre kesme işlemlerini tutarlı bir şekilde yönetir - bu mantığı her kod tabanında tekrar etmeden.


14. Adapter Deseni

Çözdüğü problem: Uyumsuz arayüzlerin birlikte çalışmasına yardımcı olur - özellikle eski (legacy) bir sistemi modern bir API sözleşmesiyle entegre ederken faydalıdır.

Ne zaman kullanılır: Eski bir sistemi, üçüncü parti bir SDK’yı veya farklı şekillendirilmiş bir API’yi uygulamanızın beklediği arayüzün arkasına sararken.

// Modern uygulamanızın beklediği hedef arayüz
public interface PaymentGateway {
    PaymentResult pay(BigDecimal amount, String currency);
}

// Uyumsuz arayüze sahip eski sistem
public class LegacyPaymentSystem {
    public LegacyResponseCode processPaymentInCents(long amountInCents, String currencyCode) {
        // eski SOAP/legacy çağrısı
        return LegacyResponseCode.OK;
    }
}

// Adapter ikisi arasında köprü kurar
public class LegacyPaymentAdapter implements PaymentGateway {

    private final LegacyPaymentSystem legacySystem;

    @Override
    public PaymentResult pay(BigDecimal amount, String currency) {
        long cents = amount.multiply(BigDecimal.valueOf(100)).longValue();
        LegacyResponseCode code = legacySystem.processPaymentInCents(cents, currency);
        return code == LegacyResponseCode.OK ? PaymentResult.success() : PaymentResult.failure();
    }
}

Servisiniz sadece PaymentGateway‘e bağımlıdır - eski sistemi ileride modern bir sistemle değiştirmek, iş mantığını yeniden yazmak yerine sadece yeni bir adapter yazmak anlamına gelir.


15. Proxy Deseni

Çözdüğü problem: Bir proxy, gerçek servise erişimi kontrol eder; genellikle güvenlik (yetki kontrolleri), önbellekleme veya loglama için kullanılır - gerçek nesnenin önüne şeffaf bir şekilde davranış ekler.

Ne zaman kullanılır: Bir servisi değiştirmeden önüne erişim kontrolü, önbellekleme veya tembel yükleme (lazy loading) eklemeniz gerektiğinde.

public interface ProductService {
    Product getProduct(String id);
}

public class RealProductService implements ProductService {
    @Override
    public Product getProduct(String id) {
        return database.findById(id); // maliyetli veritabanı çağrısı
    }
}

public class CachingProductServiceProxy implements ProductService {

    private final RealProductService realService;
    private final Map<String, Product> cache = new ConcurrentHashMap<>();

    public CachingProductServiceProxy(RealProductService realService) {
        this.realService = realService;
    }

    @Override
    public Product getProduct(String id) {
        return cache.computeIfAbsent(id, realService::getProduct);
    }
}

Spring’in kendisi de bu deseni içeride kullanır - @Cacheable, @Transactional ve Spring Security metot düzeyi kontrolleri, bean’lerinizi saran dinamik proxy’ler aracılığıyla uygulanır.


16. Factory Deseni

Çözdüğü problem: Nesneleri, örnekleme (instantiation) mantığını istemciye açık etmeden oluşturur; nesne yaratma ile kullanımı arasında gevşek bağlılık sağlar.

Ne zaman kullanılır: Nesne oluşturma, çağıran koda sızmaması gereken mantık/dallanma (tipe göre implementasyon seçme) içerdiğinde.

public interface NotificationSender {
    void send(String recipient, String message);
}

public class EmailSender implements NotificationSender { /* ... */ }
public class SmsSender implements NotificationSender { /* ... */ }
public class PushSender implements NotificationSender { /* ... */ }

public class NotificationSenderFactory {
    public static NotificationSender create(NotificationType type) {
        return switch (type) {
            case EMAIL -> new EmailSender();
            case SMS -> new SmsSender();
            case PUSH -> new PushSender();
        };
    }
}

// Kullanım
NotificationSender sender = NotificationSenderFactory.create(NotificationType.SMS);
sender.send("+905551234567", "Siparişiniz kargoya verildi!");

17. Strategy Deseni

Çözdüğü problem: Birbirinin yerine geçebilen bir algoritma ailesi tanımlar ve istemcinin bunlardan birini çalışma zamanında seçmesine izin verir; büyük if/else veya switch bloklarından kaçınır.

Ne zaman kullanılır: Aynı işlemi gerçekleştirmenin birden fazla yolu olduğunda (fiyatlandırma kuralları, kargo maliyeti hesaplama, indirim stratejileri).

public interface ShippingStrategy {
    BigDecimal calculate(Order order);
}

public class StandardShipping implements ShippingStrategy {
    public BigDecimal calculate(Order order) { return BigDecimal.valueOf(5.99); }
}

public class ExpressShipping implements ShippingStrategy {
    public BigDecimal calculate(Order order) { return BigDecimal.valueOf(15.99); }
}

public class ShippingCalculator {
    private final ShippingStrategy strategy;

    public ShippingCalculator(ShippingStrategy strategy) {
        this.strategy = strategy; // çalışma zamanında enjekte edilir, ör. Spring @Qualifier ile
    }

    public BigDecimal calculateCost(Order order) {
        return strategy.calculate(order);
    }
}

18. Observer Deseni

Çözdüğü problem: Bir özne (subject) durum değiştirdiğinde, birden fazla gözlemcinin (observer) otomatik olarak bilgilendirildiği bire-çok bağımlılık kurar.

Ne zaman kullanılır: Süreç içi (in-process) olay bildirimi için (servisler arası eşdeğeri olan Olay Güdümlü Mimari’nin aksine). Spring’de yaygın olarak ApplicationEventPublisher ile kullanılır.

// Olay
public record OrderPlacedEvent(String orderId, BigDecimal total) {}

// Özne yayınlar
@Service
public class OrderService {
    private final ApplicationEventPublisher publisher;

    public void placeOrder(Order order) {
        orderRepository.save(order);
        publisher.publishEvent(new OrderPlacedEvent(order.getId(), order.getTotal()));
    }
}

// Birden fazla gözlemci bağımsız olarak tepki verir
@Component
public class LoyaltyPointsObserver {
    @EventListener
    public void onOrderPlaced(OrderPlacedEvent event) {
        loyaltyService.addPoints(event.orderId(), event.total());
    }
}

@Component
public class AnalyticsObserver {
    @EventListener
    public void onOrderPlaced(OrderPlacedEvent event) {
        analyticsService.track(event);
    }
}

19. Singleton Deseni

Çözdüğü problem: Bir sınıfın yalnızca tek bir örneğinin olmasını garanti eder ve bu örneğe genel bir erişim noktası sağlar.

Ne zaman kullanılır: Paylaşılan, durumsuz (veya dikkatlice senkronize edilmiş) kaynaklar için (yapılandırma tutucular, bağlantı havuzları gibi). Not: Spring’de her @Bean/@Service varsayılan olarak singleton’dır - bunu elle yazmanız nadiren gerekir.

public class ConfigurationManager {

    private static volatile ConfigurationManager instance;
    private final Map<String, String> settings = new ConcurrentHashMap<>();

    private ConfigurationManager() { /* ayarları yükle */ }

    public static ConfigurationManager getInstance() {
        if (instance == null) {
            synchronized (ConfigurationManager.class) {
                if (instance == null) {
                    instance = new ConfigurationManager();
                }
            }
        }
        return instance;
    }
}
// İdiomatik Spring yolu - singleton kapsamı zaten varsayılandır:
@Service // Spring container tarafından yönetilen tek, paylaşılan bir örnek
public class ConfigurationService {
    // ...
}

20. Builder Deseni

Çözdüğü problem: Karmaşık nesneleri adım adım inşa eder; birçok opsiyonel alana sahip nesneler için (immutable nesneler, DTO’lar) uzayan (telescoping) constructor’lara gerek kalmadan kullanışlıdır.

Ne zaman kullanılır: Çok sayıda parametreye, özellikle opsiyonel parametrelere sahip nesnelerde veya okunabilir bir kurulum koduyla immutability isteniyorsa.

public final class OrderRequest {
    private final String customerId;
    private final List<OrderItem> items;
    private final String couponCode; // opsiyonel
    private final boolean giftWrap;  // opsiyonel

    private OrderRequest(Builder builder) {
        this.customerId = builder.customerId;
        this.items = builder.items;
        this.couponCode = builder.couponCode;
        this.giftWrap = builder.giftWrap;
    }

    public static class Builder {
        private String customerId;
        private List<OrderItem> items = new ArrayList<>();
        private String couponCode;
        private boolean giftWrap = false;

        public Builder customerId(String customerId) { this.customerId = customerId; return this; }
        public Builder items(List<OrderItem> items) { this.items = items; return this; }
        public Builder couponCode(String couponCode) { this.couponCode = couponCode; return this; }
        public Builder giftWrap(boolean giftWrap) { this.giftWrap = giftWrap; return this; }

        public OrderRequest build() { return new OrderRequest(this); }
    }
}

// Kullanım
OrderRequest request = new OrderRequest.Builder()
    .customerId("cust-123")
    .items(cartItems)
    .couponCode("SAVE10")
    .giftWrap(true)
    .build();

Modern Java’da bu boilerplate kodu elle yazmak yerine genellikle record + bir builder kütüphanesi (Lombok’un @Builder‘ı gibi) kullanılır.


21. Decorator Deseni

Çözdüğü problem: Bir nesnenin kodunu değiştirmeden, onu bir veya daha fazla dekoratör katmanına sararak davranışını dinamik olarak genişletir.

Ne zaman kullanılır: Çekirdek bir implementasyonun etrafına, kompoze edilebilir şekilde çapraz kesen davranış (loglama, önbellekleme, doğrulama) eklerken.

public interface OrderProcessor {
    void process(Order order);
}

public class BasicOrderProcessor implements OrderProcessor {
    @Override
    public void process(Order order) { /* temel işleme mantığı */ }
}

public class LoggingOrderProcessorDecorator implements OrderProcessor {
    private final OrderProcessor delegate;
    public LoggingOrderProcessorDecorator(OrderProcessor delegate) { this.delegate = delegate; }

    @Override
    public void process(Order order) {
        log.info("Sipariş işleniyor {}", order.getId());
        delegate.process(order);
        log.info("Sipariş tamamlandı {}", order.getId());
    }
}

public class ValidatingOrderProcessorDecorator implements OrderProcessor {
    private final OrderProcessor delegate;
    public ValidatingOrderProcessorDecorator(OrderProcessor delegate) { this.delegate = delegate; }

    @Override
    public void process(Order order) {
        if (order.getItems().isEmpty()) throw new IllegalArgumentException("Boş sipariş");
        delegate.process(order);
    }
}

// Dekoratörleri birleştir
OrderProcessor processor = new LoggingOrderProcessorDecorator(
    new ValidatingOrderProcessorDecorator(
        new BasicOrderProcessor()));
processor.process(order);

22. Repository Deseni

Çözdüğü problem: Veri erişim mantığını koleksiyon benzeri bir arayüzün arkasına soyutlar; kodu daha temiz ve (repository mock’lanarak) birim testi daha kolay hale getirir.

Ne zaman kullanılır: Spring Data uygulamalarında hemen hemen her zaman - kalıcılık detaylarını iş mantığından ayırmanın standart yoludur.

public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByCustomerId(String customerId);
    Optional<Order> findByIdAndStatus(Long id, OrderStatus status);
}

@Service
public class OrderService {

    private final OrderRepository orderRepository; // iş mantığı asla doğrudan SQL/JDBC'ye dokunmaz

    public List<Order> getCustomerOrders(String customerId) {
        return orderRepository.findByCustomerId(customerId);
    }
}

Spring Data JPA, OrderRepository‘nin implementasyonunu çalışma zamanında metot adı kurallarından otomatik olarak üretir - manuel DAO boilerplate’ine gerek yoktur.


23. Dependency Injection Deseni

Çözdüğü problem: Bağımlılıklar içeride oluşturulmak yerine dışarıdan sağlanır; bu da gevşek bağlılık ve test edilebilirliği artırır.

Ne zaman kullanılır: Spring’de evrensel olarak - bu, framework’ün temelidir (“IoC container”).

public interface NotificationService {
    void notify(String userId, String message);
}

@Service
public class EmailNotificationService implements NotificationService {
    @Override
    public void notify(String userId, String message) { /* e-posta gönder */ }
}

@Service
public class OrderService {

    private final NotificationService notificationService; // enjekte edilir, "new EmailNotificationService()" değil

    // Constructor injection - field injection'a tercih edilir
    public OrderService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void completeOrder(Order order) {
        // ...
        notificationService.notify(order.getCustomerId(), "Sipariş tamamlandı!");
    }
}

Bir testte, OrderService‘in koduna hiç dokunmadan sahte (mock) bir NotificationService enjekte edebilirsiniz - DI’ın tüm amacı da budur.


24. Outbox Deseni

Çözdüğü problem: Veritabanı güncellemesi ile mesaj broker’ına yayınlama arasında servis çökse bile güvenilir olay yayınlamayı garanti eder; olay, aynı veritabanı işlemi (transaction) içinde bir “outbox” tablosuna yazılır.

Ne zaman kullanılır: Bir servisin kendi veritabanını atomik olarak güncellemesi VE bir olay yayınlaması gerektiğinde - klasik “çift yazma” (dual write) probleminden kaçınmak için.

Yaygın araç: Debezium (CDC) + Kafka veya polling tabanlı bir yayıncı.

@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        orderRepository.save(order); // 1. iş işlemi (veritabanı güncellemesi)

        OutboxEvent event = new OutboxEvent(
            UUID.randomUUID().toString(),
            "Order",
            order.getId().toString(),
            "OrderCreated",
            toJson(order)
        );
        outboxRepository.save(event); // 2. outbox tablosuna yaz - AYNI transaction, AYNI commit
    }
}
// 3. Ayrı bir poller (veya Debezium CDC) broker'a yayınlar
@Scheduled(fixedDelay = 500)
public void publishOutboxEvents() {
    List<OutboxEvent> pending = outboxRepository.findUnpublished();
    for (OutboxEvent event : pending) {
        kafkaTemplate.send("order-events", event.getPayload());
        outboxRepository.markPublished(event.getId());
    }
}
  1. ve 2. adım aynı veritabanı işlemi içinde gerçekleştiğinden, ikisi de ya başarılı olur ya da ikisi de geri alınır - siparişin kaydedilip olayın sessizce kaybolduğu bir durum asla oluşmaz.

25. Idempotency (Eş Güçlülük) Deseni

Çözdüğü problem: Yinelenen isteklerin (istemci yeniden denemeleri, ağ zaman aşımları veya mesajın tekrar iletilmesi kaynaklı) yinelenen işlemeye neden olmamasını sağlar (ör. müşteriden iki kez para çekilmesi).

Ne zaman kullanılır: İstemcinin yeniden deneyebileceği herhangi bir işlemde veya en-az-bir-kez (at-least-once) teslimat yapan bir kuyruktan (Kafka gibi) okuyan herhangi bir mesaj tüketicisinde.

@RestController
public class PaymentController {

    private final IdempotencyKeyRepository idempotencyRepo;
    private final PaymentService paymentService;

    @PostMapping("/payments")
    public ResponseEntity<PaymentResult> charge(
            @RequestHeader("Idempotency-Key") String idempotencyKey,
            @RequestBody ChargeRequest request) {

        Optional<PaymentResult> existing = idempotencyRepo.findResult(idempotencyKey);
        if (existing.isPresent()) {
            return ResponseEntity.ok(existing.get()); // önceki yanıtı döndür, yeniden işleme
        }

        PaymentResult result = paymentService.charge(request);
        idempotencyRepo.save(idempotencyKey, result);
        return ResponseEntity.ok(result);
    }
}
// Kafka tüketicileri için de aynı prensip (en-az-bir-kez teslimat)
@KafkaListener(topics = "payment-events")
public void handle(PaymentEvent event) {
    if (processedEventRepository.existsById(event.getEventId())) {
        return; // zaten işlendi - atla
    }
    paymentService.process(event);
    processedEventRepository.save(new ProcessedEvent(event.getEventId()));
}

Mikroservislerin Arkasındaki Temel Prensipler

PrensipNe anlama gelir
Merkezi Olmayan Veri YönetimiHer servis kendi verisine sahiptir; paylaşılan veritabanı yoktur.
Bağımsız Dağıtım (Deployment)Servisler, tüm sistemi yeniden dağıtmadan bağımsız olarak dağıtılabilir.
Hata İzolasyonuBir servisteki hata diğerlerine zincirleme yayılmamalıdır.
Gevşek BağlılıkServisler iç detaylar yerine iyi tanımlanmış sözleşmeler (API/olaylar) üzerinden etkileşir.
Yüksek ErişilebilirlikYedeklilik ve dayanıklılık (resilience) desenleri, kısmi hatalara rağmen sistemin çalışmasını sağlar.

Yaygın Araçlar ve Teknoloji Yığını

KategoriAraçlar
Dayanıklılık (Resilience)Resilience4j, Hystrix (eski)
Servis KeşfiEureka, Consul
API GatewaySpring Cloud Gateway, Kong
MesajlaşmaApache Kafka, RabbitMQ
GözlemlenebilirlikJaeger, Zipkin, ELK Stack, Prometheus, Grafana
Konteyner/OrkestrasyonDocker, Kubernetes
Yapılandırma YönetimiSpring Cloud Config, Consul

İyi bir mimari, bugün doğru ödünleşimleri (trade-off) yaparak daha iyi bir yarın inşa etmektir. Akıllıca tasarla. Sağlam inşa et. Sınırsızca ölçekle.