Teknoloji AI Üretimi

Derinlemesine İnceleme: Java Spring Boot ile Event-Driven (Olay Yönelimli) Microservice Tasarımı - Producer/Consumer Kodları ve Event Akış SVG'si

Giriş: Olay Yönelimli Mimari Neden Kritik?

Olay yönelimli mimari (Event-Driven Architecture - EDA), modern dağıtık sistemlerin bel kemiğini oluşturan bir paradigmadır. Neden mi? Çünkü monolitik uygulamaların aksine, EDA;

  • Yüksek ölçeklenebilirlik sağlar: Producer ve consumer'lar bağımsız olarak ölçeklenebilir.
  • Zayıf bağlılık (loose coupling) sunar: Servisler birbirlerinin varlığından habersizdir, sadece olayları dinler.
  • Gerçek zamanlı veri işleme imkanı verir: Anlık bildirimler, fraud detection, IoT sinyalleri gibi use-case'ler için idealdir.

Ancak, EDA'nın yanlış uygulanması, prodüksiyonda felaketlere yol açabilir. Bu makalede, Spring Boot 3.x ile prodüksiyon seviyesinde bir EDA uygulaması nasıl inşa edilir, adım adım inceleyeceğiz.


1. Temel Kavramlar ve Mimarinin Omurgası

1.1. Event-Driven Mimari Bileşenleri

Bir EDA sisteminde üç ana bileşen bulunur:

  1. Producer (Olay Üretici): Olayı yayınlayan servis (örn: OrderServiceOrderCreatedEvent).
  2. Event Bus (Olay Aracısı): Olayları taşıyan altyapı (örn: Apache Kafka, RabbitMQ, AWS SNS/SQS).
  3. Consumer (Olay Tüketicisi): Olayı dinleyen ve işleyen servis (örn: NotificationService, InventoryService).
// Örnek Event Sınıfı (Immutable, serializable)
public record OrderCreatedEvent(
    UUID orderId,
    UUID customerId,
    List items,
    Instant createdAt
) implements Serializable {
    public record OrderItem(UUID productId, int quantity) {}
}
🚨 Prodüksiyon Faciası Event sınıflarını Serializable yapmazsanız, Kafka veya RabbitMQ gibi broker'lar olayları deserialize edemez. Ayrıca, record kullanmak immutable ve thread-safe olaylar sağlar, ancak JSON serileştirme için @JsonCreator veya jackson-databind uyumluluğunu kontrol edin.

1.2. Event Akış Diyagramı (SVG)

Aşağıda, tipik bir EDA akışını gösteren SVG diyagramı yer alıyor. Bu diyagram, makalenin sonunda detaylı olarak açıklanacak:

<svg viewBox="0 0 800 400" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#8b5cf6"></polygon>
    </marker>
    <linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" stop-color="#0f172a"></stop>
      <stop offset="100%" stop-color="#1e1b4b"></stop>
    </linearGradient>
  </defs>
  <rect width="800" height="400" fill="url(#gradient)"></rect>
  <g stroke="#8b5cf6" stroke-width="2" fill="none" marker-end="url(#arrowhead)">
    <rect x="50" y="150" width="150" height="100" rx="5" fill="rgba(139, 92, 246, 0.2)"></rect>
    <text x="125" y="200" text-anchor="middle" fill="#e5e7eb" font-family="Arial" font-size="14">Order Service
(Producer)</text>
    <rect x="300" y="50" width="200" height="300" rx="5" fill="rgba(139, 92, 246, 0.1)"></rect>
    <text x="400" y="30" text-anchor="middle" fill="#e5e7eb" font-family="Arial" font-size="16">Event Bus
(Kafka/RabbitMQ)</text>
    <rect x="600" y="100" width="150" height="200" rx="5" fill="rgba(139, 92, 246, 0.2)"></rect>
    <text x="675" y="120" text-anchor="middle" fill="#e5e7eb" font-family="Arial" font-size="12">Notification
Service</text>
    <text x="675" y="150" text-anchor="middle" fill="#e5e7eb" font-family="Arial" font-size="12">(Consumer)</text>
    <text x="675" y="180" text-anchor="middle" fill="#e5e7eb" font-family="Arial" font-size="12">Inventory
Service</text>
    <text x="675" y="210" text-anchor="middle" fill="#e5e7eb" font-family="Arial" font-size="12">(Consumer)</text>
    <path d="M200 200 L300 200"></path>
    <path d="M500 120 L600 120"></path>
    <path d="M500 180 L600 180"></path>
    <path d="M500 240 L600 240"></path>
  </g>
</svg>

2. Spring Boot ile Producer Uygulaması

2.1. Gerekli Bağımlılıklar

Spring Boot 3.x ile Kafka kullanmak için pom.xml dosyasına aşağıdaki bağımlılıkları ekleyin:


    org.springframework.kafka
    spring-kafka
    3.1.0


    com.fasterxml.jackson.core
    jackson-databind
💡 Mimari Karar spring-kafka yerine spring-cloud-stream kullanmayı düşünebilirsiniz. Ancak, low-level kontrol istiyorsanız (örn: custom partitioner, retry policies), doğrudan spring-kafka tercih edin. spring-cloud-stream, abstraction sağlar ama esnekliği azaltır.

2.2. Kafka Producer Konfigürasyonu

application.yml dosyasında Kafka ayarlarını yapılandırın:

spring:
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
      retries: 3
      acks: all
ℹ️ Best Practice acks: all kullanarak, producer'ın mesajın tüm replica'lara yazıldığından emin olmasını sağlayın. Bu, veri kaybını önler ama latency artabilir. Trade-off'ı iyi değerlendirin.

2.3. Event Yayınlama (Producer Kodları)

OrderService sınıfı, bir sipariş oluşturulduğunda OrderCreatedEvent yayınlayacak:

@Service
@RequiredArgsConstructor
public class OrderService {
    private final KafkaTemplate kafkaTemplate;
    private final String TOPIC_NAME = "orders";

    public UUID createOrder(OrderRequest request) {
        UUID orderId = UUID.randomUUID();
        OrderCreatedEvent event = new OrderCreatedEvent(
            orderId,
            request.customerId(),
            request.items(),
            Instant.now()
        );
        
        // Async olarak event yayınla
        kafkaTemplate.send(TOPIC_NAME, orderId.toString(), event)
            .addCallback(
                result -&gt; log.info("Event published: {}", event),
                ex -&gt; log.error("Failed to publish event", ex)
            );
        
        return orderId;
    }
}
🚨 Kritik Uyarı KafkaTemplate.send() çağrısı non-blocking olsa da, callback'ler @Async ile çalışmaz. Eğer callback içinde uzun süren işlemler yapacaksanız, @Async veya CompletableFuture kullanın. Aksi halde, thread pool tıkanabilir.

3. Spring Boot ile Consumer Uygulaması

3.1. Kafka Consumer Konfigürasyonu

application.yml dosyasında consumer ayarlarını yapılandırın:

spring:
  kafka:
    consumer:
      group-id: notification-service-group
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      properties:
        spring.json.trusted.packages: "*"
💡 Mimari Karar auto-offset-reset: earliest kullanmak, consumer'ın baştan başlamasını sağlar. Prodüksiyonda latest tercih edin, aksi halde eski olaylar tekrar işlenir. Ayrıca, group-id'yi servis bazında benzersiz yapın (örn: notification-service-v1).

3.2. Event Dinleme ve İşleme (Consumer Kodları)

NotificationService sınıfı, OrderCreatedEvent'i dinleyecek ve müşteriye bildirim gönderecek:

@Service
@RequiredArgsConstructor
public class NotificationService {
    private final EmailSender emailSender;

    @KafkaListener(topics = "orders", groupId = "notification-service-group")
    public void handleOrderCreated(OrderCreatedEvent event) {
        String emailContent = String.format(
            "Siparişiniz alındı! Sipariş No: %s",
            event.orderId()
        );
        emailSender.send(
            event.customerId(),
            "Sipariş Onayı",
            emailContent
        );
    }
}

3.3. Hata Yönetimi ve Retry Mekanizmaları

Consumer'larda hata yönetimi kritiktir. Aşağıdaki gibi bir RetryTemplate kullanarak, transient hatalar için otomatik retry sağlayın:

@Configuration
public class KafkaConsumerConfig {
    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
        
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(1000); // 1 saniye
        retryTemplate.setBackOffPolicy(backOffPolicy);
        
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(3);
        retryTemplate.setRetryPolicy(retryPolicy);
        
        return retryTemplate;
    }
}

@Service
@RequiredArgsConstructor
public class NotificationService {
    private final RetryTemplate retryTemplate;
    
    @KafkaListener(topics = "orders", groupId = "notification-service-group")
    public void handleOrderCreated(OrderCreatedEvent event) {
        retryTemplate.execute(
            context -&gt; {
                emailSender.send(
                    event.customerId(),
                    "Sipariş Onayı",
                    "Siparişiniz alındı!"
                );
                return null;
            },
            context -&gt; {
                log.error("Failed to send email after {} retries", context.getRetryCount());
                return null;
            }
        );
    }
}
ℹ️ Best Practice Retry mekanizmalarında exponential backoff kullanın. Örneğin, ilk retry 1 saniye, ikinci 2 saniye, üçüncü 4 saniye gibi. Bu, sistemin kendini toparlaması için zaman tanır. Ayrıca, @RetryableTopic (Spring Kafka 2.7+) ile declarative retry de yapabilirsiniz.

4. Event Akışının İzlenmesi ve Observability

4.1. Metrikler ve Logging

EDA sistemlerinde observability hayati önem taşır. Aşağıdaki metrikleri mutlaka izleyin:

  • Producer Metrikleri: record-send-rate, record-error-rate, request-latency-avg.
  • Consumer Metrikleri: records-consumed-rate, records-lag-max, poll-interval-avg.

Spring Boot Actuator ile Kafka metriklerini açığa çıkarın:

management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    export:
      prometheus:
        enabled: true

4.2. Distributed Tracing

Olay akışını end-to-end trace etmek için OpenTelemetry veya Spring Cloud Sleuth kullanın:

@KafkaListener(topics = "orders", groupId = "notification-service-group")
public void handleOrderCreated(OrderCreatedEvent event, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
    Span span = tracer.nextSpan().name("process-order-event").start();
    try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
        // İşlemleri trace et
        emailSender.send(event.customerId(), "Sipariş Onayı", "...")
    } finally {
        span.finish();
    }
}
🚨 Prodüksiyon Faciası Distributed tracing olmadan, olay akışındaki hataları debug etmek imkansızdır. Özellikle consumer lag veya event duplication sorunlarında, trace ID'ler hayat kurtarır. traceparent header'ını Kafka mesajlarına eklemeyi unutmayın.

5. İleri Düzey Konular ve Prodüksiyon İpuçları

5.1. Event Schema Yönetimi

Olay şemalarını (schema) merkezi bir yerde yönetin. Apache Avro veya JSON Schema kullanarak, backward/forward compatibility sağlayın:

{
  "type": "record",
  "name": "OrderCreatedEvent",
  "fields": [
    {"name": "orderId", "type": "string"},
    {"name": "customerId", "type": "string"},
    {"name": "items", "type": {"type": "array", "items": {
      "type": "record",
      "name": "OrderItem",
      "fields": [
        {"name": "productId", "type": "string"},
        {"name": "quantity", "type": "int"}
      ]
    }}}
  ]
}
💡 Mimari Karar Schema registry (örn: **Confluent Schema Registry**) kullanarak, producer ve consumer'ların uyumlu şemalarla çalışmasını sağlayın. Bu, breaking changes'i önler ve veri bütünlüğünü korur.

5.2. Event Sourcing ve CQRS

EDA, Event Sourcing ve CQRS ile mükemmel bir uyum sağlar. Örneğin:

  • Event Sourcing: Tüm state değişikliklerini olaylar olarak saklayın.
  • CQRS: Okuma ve yazma modellerini ayırın (örn: OrderWriteService ve OrderReadService).
// Event Sourcing için basit bir repository
public interface EventStore {
    void save(Event event);
    List load(UUID aggregateId);
}

// CQRS için ayrı servisler
@Service
public class OrderWriteService {
    private final EventStore eventStore;
    
    public void createOrder(OrderRequest request) {
        OrderCreatedEvent event = new OrderCreatedEvent(...);
        eventStore.save(event);
    }
}

@Service
public class OrderReadService {
    private final EventStore eventStore;
    
    public Order getOrder(UUID orderId) {
        List events = eventStore.load(orderId);
        return Order.replay(events);
    }
}

5.3. Dead Letter Queue (DLQ) ve Poison Pill Yönetimi

Consumer'larda işlenemeyen olayları DLQ'ya gönderin:

@KafkaListener(topics = "orders.DLQ", groupId = "notification-service-group")
public void handleDeadLetter(OrderCreatedEvent event) {
    log.error("Dead letter event: {}", event);
    // Manuel müdahale veya retry mekanizması ekleyin
}

// Consumer'da DLQ'ya gönderme
@KafkaListener(topics = "orders", groupId = "notification-service-group")
public void handleOrderCreated(OrderCreatedEvent event) {
    try {
        emailSender.send(...);
    } catch (Exception e) {
        kafkaTemplate.send("orders.DLQ", event);
    }
}
ℹ️ Best Practice DLQ'ya gönderilen olayları manuel olarak inceleyin. Otomatik retry mekanizmaları, poison pill olaylarını sürekli olarak DLQ'ya gönderir ve sistemi tıkar. Bunun yerine, @DltHandler (Spring Kafka 2.7+) kullanarak DLQ işlemlerini kolaylaştırın.

6. Sonuç ve Öneriler

Olay yönelimli mimariler, dağıtık sistemlerin geleceğidir. Ancak, doğru uygulanmadığında karmaşık hatalara ve prodüksiyon felaketlerine yol açabilir. Bu makalede ele aldığımız konuları özetleyelim:

  1. Producer/Consumer Kodları: Spring Boot ile nasıl sağlam bir EDA sistemi inşa edeceğinizi gördük.
  2. Hata Yönetimi: Retry mekanizmaları, DLQ ve observability ile sisteminizi dayanıklı hale getirin.
  3. İleri Düzey Konular: Event sourcing, CQRS ve schema yönetimi ile mimarinizi bir üst seviyeye taşıyın.

Öneriler:

  • Kafka yerine RabbitMQ kullanmayı düşünüyorsanız, message durability ve partitioning konularında dikkatli olun.
  • Event duplication sorununu çözmek için idempotent consumer'lar tasarlayın.
  • Consumer lag'i izlemek için Prometheus + Grafana kullanın.
  • Schema evolution için Avro veya Protobuf tercih edin.
🎯 Başarı İçin İpucu EDA sistemlerini tasarlarken, "What if?" sorularını sorun: - "What if the event bus goes down?" - "What if the consumer crashes mid-processing?" - "What if the event schema changes?"

Bu sorulara cevap veren bir mimari, prodüksiyonda başarıya ulaşır.


Ek: Event Akış SVG'si (Detaylı Açıklama)

Makalenin başında yer alan SVG diyagramı, tipik bir EDA akışını göstermektedir:

  1. Producer (Order Service): Sipariş oluşturulduğunda OrderCreatedEvent yayınlar.
  2. Event Bus (Kafka/RabbitMQ): Olayı taşır ve consumer'lara iletir.
  3. Consumer'lar (Notification/Inventory Service): Olayı dinler ve işler.

Diyagramdaki renk kodları ve geometrik şekiller şu anlamlara gelir:

  • Mor tonları: Event akışını temsil eder.
  • Oklar: Olayın yönünü gösterir.
  • Dikdörtgenler: Servisleri temsil eder.

Bu diyagram, sistem mimarinizi dokümante etmek için de kullanılabilir. SVG formatı, scalable ve editlenebilir olduğu için tercih edilir.

Etiketler

Bu yazı nasıldı? Bir emoji bırak!

Yorumlar

0 Yorum

Bir Yorum Bırakın

💬

Henüz yorum yapılmamış. İlk yorumu siz yapın!