Mikroservis Mimarisi & Veri Agregasyonu 7 dk okuma 2 okunma

API Composition Pattern

API Composition Pattern, bir istemcinin tek bir isteğini karşılamak için birden fazla mikroservisten veri toplayıp birleştiren bir mimari desendir. Bu yazıda, desenin çalışma prensibi, veri toplama stratejileri (paralel, sıralı), hata yönetimi, performans optimizasyonları, CQRS ve BFF ile ilişkisi, .NET'te uygulama yöntemleri ve en iyi pratikler ele alınır.

API Composition Pattern

API Composition Pattern: Birden Çok Servisten Veri Toplama

Mikroservis mimarisinde, her servis kendi veritabanına ve kendi sorumluluğuna sahiptir. Ancak bir istemci (web, mobil), genellikle tek bir istekte birden fazla servisten gelen verilere ihtiyaç duyar. Örneğin, bir e-ticaret uygulamasında sipariş detay sayfası; Sipariş Servisi'nden sipariş bilgilerini, Müşteri Servisi'nden müşteri bilgilerini, Ürün Servisi'nden ürün detaylarını ve Kargo Servisi'nden kargo durumunu gerektirir. İşte API Composition Pattern, bu ihtiyacı karşılamak için birden fazla servisten veri toplayıp tek bir yanıtta birleştiren bir çözüm sunar.


1. API Composition Pattern Nedir?

API Composition, bir Composer (Birleştirici) bileşeninin, istemciden gelen tek bir isteği alıp, bu isteği karşılamak için gerekli olan birden fazla mikroservise çağrı yapması ve sonuçları birleştirerek tek bir yanıt dönmesidir.

Temel Akış:

text

İstemci → API Composer → [Servis A, Servis B, Servis C]
                ↓
        Birleştirilmiş Yanıt → İstemci

Composer'ın Sorumlulukları:

  • İstemciden gelen isteği analiz etmek.

  • Hangi servislerin çağrılacağını belirlemek.

  • Servis çağrılarını (paralel veya sıralı) yapmak.

  • Gelen yanıtları birleştirmek (join, merge).

  • Hataları yönetmek (fallback, partial response).

  • Sonucu istemciye uygun formatta dönmek.


2. API Composition vs. API Gateway vs. BFF

Bu üç desen sıklıkla karıştırılır. Aralarındaki farkı netleştirelim:

Desen Amaç Veri Birleştirme Kapsam
API Gateway Tek giriş noktası, routing, auth, rate limiting ❌ Genelde yok Tüm istemciler
BFF İstemciye özel backend katmanı ✅ Var Tek istemci türü (web, mobil)
API Composition Birden fazla servisten veri toplama ✅ Var (odak) Belirli bir iş senaryosu

İlişki: API Composition genellikle BFF katmanı içinde veya API Gateway'in arkasında uygulanır. BFF, istemciye özel optimizasyon yaparken; API Composition, veri toplama mantığını üstlenir.


3. Veri Toplama Stratejileri

A. Paralel Çağrılar (Paralel Composition)
Bağımsız servis çağrıları aynı anda yapılır. Toplam süre, en yavaş servisin süresine eşittir.

csharp

public async Task<OrderDetailDto> GetOrderDetailAsync(int orderId)
{
    // Tüm çağrıları paralel başlat
    var orderTask = _orderService.GetOrderAsync(orderId);
    var customerTask = _customerService.GetCustomerByOrderAsync(orderId);
    var itemsTask = _productService.GetItemsByOrderAsync(orderId);
    var shipmentTask = _shipmentService.GetShipmentByOrderAsync(orderId);

    // Hepsini bekle
    await Task.WhenAll(orderTask, customerTask, itemsTask, shipmentTask);

    // Sonuçları birleştir
    return new OrderDetailDto
    {
        Order = await orderTask,
        Customer = await customerTask,
        Items = await itemsTask,
        Shipment = await shipmentTask
    };
}

Avantaj: Toplam gecikme minimumdur.
Dezavantaj: Servisler arasında bağımlılık varsa uygulanamaz.

B. Sıralı Çağrılar (Sequential Composition)
Bir servisin yanıtı, diğer servis çağrısı için gerekliyse sıralı yapılır. Toplam süre, tüm servislerin sürelerinin toplamıdır.

csharp

public async Task<OrderDetailDto> GetOrderDetailAsync(int orderId)
{
    // 1. Siparişi al
    var order = await _orderService.GetOrderAsync(orderId);

    // 2. Siparişin müşteri ID'si ile müşteriyi al
    var customer = await _customerService.GetCustomerAsync(order.CustomerId);

    // 3. Sipariş kalemlerini al
    var items = await _productService.GetItemsByOrderAsync(orderId);

    return new OrderDetailDto { Order = order, Customer = customer, Items = items };
}

Avantaj: Bağımlı veriler için gereklidir.
Dezavantaj: Toplam gecikme yüksektir.

C. Hibrit Yaklaşım (En İyi Pratik)
Bağımsız çağrıları paralel, bağımlı çağrıları sıralı yapın.

csharp

public async Task<OrderDetailDto> GetOrderDetailAsync(int orderId)
{
    // 1. Siparişi al (bağımsız)
    var order = await _orderService.GetOrderAsync(orderId);

    // 2. Siparişe bağlı çağrıları paralel başlat
    var customerTask = _customerService.GetCustomerAsync(order.CustomerId);
    var itemsTask = _productService.GetItemsByOrderAsync(orderId);
    var shipmentTask = _shipmentService.GetShipmentByOrderAsync(orderId);

    await Task.WhenAll(customerTask, itemsTask, shipmentTask);

    return new OrderDetailDto
    {
        Order = order,
        Customer = await customerTask,
        Items = await itemsTask,
        Shipment = await shipmentTask
    };
}

4. Hata Yönetimi ve Dayanıklılık

API Composition'da en kritik konu, servislerden biri başarısız olduğunda ne olacağıdır.

A. Fail-Fast (Hızlı Başarısız)
Herhangi bir servis çağrısı başarısız olursa, tüm istek başarısız olur.

csharp

var order = await _orderService.GetOrderAsync(orderId); // Hata fırlatırsa tüm istek durur

Ne Zaman Kullanılır? Tüm veriler kritikse (ör. ödeme işlemi).

B. Graceful Degradation (Zarif Bozulma)
Kritik olmayan servisler başarısız olursa, kısmi yanıt dönülür.

csharp

public async Task<OrderDetailDto> GetOrderDetailAsync(int orderId)
{
    var order = await _orderService.GetOrderAsync(orderId); // Kritik - hata fırlatırsa dur
    var items = await _productService.GetItemsByOrderAsync(orderId); // Kritik

    // Kritik olmayan servisler - hata durumunda null dön
    var customer = await TryGetAsync(() => _customerService.GetCustomerAsync(order.CustomerId));
    var shipment = await TryGetAsync(() => _shipmentService.GetShipmentByOrderAsync(orderId));

    return new OrderDetailDto
    {
        Order = order,
        Items = items,
        Customer = customer, // null olabilir
        Shipment = shipment  // null olabilir
    };
}

private async Task<T?> TryGetAsync<T>(Func<Task<T>> action) where T : class
{
    try
    {
        return await action();
    }
    catch (Exception ex)
    {
        _logger.LogWarning(ex, "Kritik olmayan servis çağrısı başarısız oldu.");
        return null;
    }
}

Ne Zaman Kullanılır? Bazı veriler opsiyonel ise (ör. kargo durumu henüz oluşmamışsa).

C. Fallback (Yedek Değer)
Servis çağrısı başarısız olursa, önbellekten veya varsayılan bir değerden yanıt üretilir.

csharp

var customer = await _customerService.GetCustomerAsync(order.CustomerId)
    .WithFallback(() => _cache.Get<CustomerDto>($"customer:{order.CustomerId}"))
    .WithFallback(() => new CustomerDto { Name = "Bilinmiyor" });

Polly ile Retry ve Circuit Breaker:

csharp

var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(1));

var circuitBreakerPolicy = Policy
    .Handle<HttpRequestException>()
    .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));

var customer = await retryPolicy.WrapAsync(circuitBreakerPolicy)
    .ExecuteAsync(() => _customerService.GetCustomerAsync(order.CustomerId));

5. Performans Optimizasyonları

A. Paralel Çağrılar (Task.WhenAll)
Bağımsız çağrıları paralel yaparak toplam gecikmeyi azaltın.

B. Caching (Önbellekleme)
Sık kullanılan verileri önbelleğe alarak servis çağrılarını azaltın.

csharp

var customer = await _cache.GetOrSetAsync(
    $"customer:{order.CustomerId}",
    () => _customerService.GetCustomerAsync(order.CustomerId),
    TimeSpan.FromMinutes(10));

C. Batch Çağrılar
Birden fazla kayıt için tek tek çağrı yapmak yerine toplu çağrı yapın.

csharp

// ❌ N+1 problemi
foreach (var order in orders)
{
    var customer = await _customerService.GetCustomerAsync(order.CustomerId);
}

// ✅ Toplu çağrı
var customerIds = orders.Select(o => o.CustomerId).Distinct();
var customers = await _customerService.GetCustomersAsync(customerIds);

D. Timeout Yönetimi
Her servis çağrısı için timeout belirleyin; bir servisin yavaşlaması tüm isteği bloklamasın.

csharp

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var customer = await _customerService.GetCustomerAsync(order.CustomerId, cts.Token);

6. CQRS ile API Composition İlişkisi

API Composition, sıklıkla CQRS (Command Query Responsibility Segregation) ile birlikte kullanılır. Okuma tarafında (Query), veriler farklı servislerden toplanarak bir Read Model (Okuma Modeli) oluşturulur.

Materialized View Yaklaşımı:

  • Servisler, veri değişikliklerini event olarak yayınlar.

  • Bir Projection servisi, bu event'leri dinleyerek denormalize bir okuma modeli oluşturur.

  • API Composition, bu okuma modelinden tek bir sorgu ile veri çeker (daha hızlı).

Avantaj: Gerçek zamanlı veri toplama yerine, önceden hazırlanmış bir modelden okuma yapılır.
Dezavantaj: Eventual Consistency (Nihai Tutarlılık) kabul edilmelidir.


7. .NET'te API Composition Uygulaması

A. Basit Composer Servisi

csharp

public interface IOrderComposer
{
    Task<OrderDetailDto> ComposeOrderDetailAsync(int orderId);
}

public class OrderComposer : IOrderComposer
{
    private readonly IOrderService _orderService;
    private readonly ICustomerService _customerService;
    private readonly IProductService _productService;
    private readonly ILogger<OrderComposer> _logger;

    public async Task<OrderDetailDto> ComposeOrderDetailAsync(int orderId)
    {
        var order = await _orderService.GetOrderAsync(orderId);

        var customerTask = _customerService.GetCustomerAsync(order.CustomerId);
        var itemsTask = _productService.GetItemsByOrderAsync(orderId);

        await Task.WhenAll(customerTask, itemsTask);

        return new OrderDetailDto
        {
            Order = order,
            Customer = await customerTask,
            Items = await itemsTask
        };
    }
}

B. Ocelot / YARP ile API Gateway'de Composition
YARP, doğrudan composition desteği sunmasa da, özel middleware ile uygulanabilir.

C. GraphQL ile Composition
GraphQL, API Composition için doğal bir çözümdür. Hot Chocolate ile birden fazla kaynağı tek bir şema altında birleştirebilirsiniz.

csharp

public class Query
{
    public async Task<OrderDetailDto> GetOrderDetail(
        int orderId,
        [Service] IOrderService orderService,
        [Service] ICustomerService customerService,
        [Service] IProductService productService)
    {
        var order = await orderService.GetOrderAsync(orderId);
        var customerTask = customerService.GetCustomerAsync(order.CustomerId);
        var itemsTask = productService.GetItemsByOrderAsync(orderId);

        await Task.WhenAll(customerTask, itemsTask);

        return new OrderDetailDto
        {
            Order = order,
            Customer = await customerTask,
            Items = await itemsTask
        };
    }
}

8. En İyi Pratikler

Pratik Açıklama
Paralel çağrıları tercih edin Bağımsız servis çağrılarını Task.WhenAll ile paralel yapın.
Hata toleransı ekleyin Polly ile retry, circuit breaker ve fallback uygulayın.
Kritik ve kritik olmayan verileri ayırın Kritik olmayan servisler başarısız olursa kısmi yanıt dönün.
Caching kullanın Sık kullanılan verileri önbelleğe alarak servis çağrılarını azaltın.
Timeout belirleyin Her servis çağrısı için makul bir timeout ayarlayın.
Batch çağrılar yapın N+1 probleminden kaçının; toplu veri çekin.
Observability sağlayın Her servis çağrısını loglayın ve izleyin (distributed tracing).
GraphQL değerlendirin Esnek veri toplama ihtiyacı varsa GraphQL güçlü bir alternatiftir.
Composer'ı ince tutun Composer sadece veri toplama ve birleştirme yapmalı; iş mantığı servislerde kalmalı.

9. Ne Zaman API Composition Kullanmalısınız?

Kullanın:

  • İstemci, tek bir istekte birden fazla servisten veriye ihtiyaç duyuyorsa.

  • Mobil istemciler, ağ gecikmesini azaltmak için tek çağrıda tüm veriyi istiyorsa.

  • BFF katmanı oluşturuyorsanız.

  • Veri toplama mantığını merkezileştirmek istiyorsanız.

Kullanmayın:

  • Servisler arasında sıkı bağımlılık varsa (bu durumda servisleri birleştirmeyi düşünün).

  • Gerçek zamanlı veri tutarlılığı kritikse (eventual consistency kabul edilemiyorsa).

  • Basit bir CRUD işlemi için gereksiz karmaşıklık yaratıyorsa.

Sonuç

API Composition Pattern, mikroservis mimarisinde istemcinin tek bir istekte birden fazla servisten veri almasını sağlayan güçlü bir çözümdür. Paralel çağrılar, hata toleransı (Polly), caching ve batch çağrılar ile performansı optimize edebilir; graceful degradation ile kullanıcı deneyimini koruyabilirsiniz. CQRS ve GraphQL ile birlikte kullanıldığında daha da güçlü hale gelir. Unutmayın: Composer'ı ince tutun, iş mantığını servislerde bırakın ve her zaman hata senaryolarını planlayın.

Tüm yazılar Soru ya da yorumun mu var? Yaz