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.