External API Call Basics
Bu modülde MiniBank'ın kendi veritabanı ve kendi servis katmanı dışına çıkıp başka sistemlerle konuşmasını ele alıyoruz. Bir dış servise HTTP isteği göndermek ilk bakışta basit görünür: URL'i çağır, cevabı al, devam et. Production'da ise iş bu kadar masum değildir. Karşı servis yavaşlayabilir, network kesilebilir, 500 dönebilir, cevap formatı değişebilir, timeout olmazsa thread'ler bekleyebilir ve yanlış yerde yapılan external çağrı açık bir transaction'ı gereksiz yere uzatabilir.
Bu yüzden bu modülün amacı sadece WebClient ile istek atmak değil; external API çağrısını bir production boundary olarak okumaktır. Modülün sonunda MiniBank transfer akışına Fraud Check servisi ekleyeceğiz; URL, timeout, retry ve hata dönüşümünü configuration üzerinden yöneteceğiz.
Önce kavramları sadeleştirelim
External API konusunu iyi anlamak için önce konuşacağımız kelimeleri netleştirelim. Bu kavramlar sadece teknik terim değil; kodun nerede güvenli, nerede kırılgan olacağını belirleyen tasarım kararlarıdır.
External API
Uygulamanın dışında çalışan, HTTP veya benzeri bir protokol üzerinden çağırdığımız servistir. MiniBank için fraud kontrol, döviz kuru, müşteri risk skoru veya kimlik doğrulama servisi external API olabilir.
HTTP Client
Başka bir servise istek gönderen kod parçasıdır. İyi tasarlanmış client, sadece çağrı yapmaz; timeout, header, error mapping ve logging davranışını da standartlaştırır.
Client DTO
Dış servise gönderilen veya dış servisten alınan veri modelidir. Domain entity ile aynı tutulmamalıdır; çünkü dış servis contract'ı ile iç domain modelinin değişim sebepleri farklıdır.
Timeout
Karşı servis cevap vermezse en fazla ne kadar bekleyeceğimizi belirler. Timeout yoksa problem sadece karşı serviste kalmaz; bizim uygulamanın thread ve connection kaynakları da bekleyerek tükenebilir.
Retry
Geçici hata durumunda isteği yeniden deneme stratejisidir. 503 veya network hatası için anlamlı olabilir; fakat 400 Bad Request'i tekrar tekrar göndermek sadece sistemi yorar.
Idempotency
Aynı isteğin birden fazla kez gönderildiğinde aynı sonucu üretmesidir. Retry kararında önemlidir; para transferi gibi yazma operasyonlarında idempotency yoksa retry ciddi risk oluşturabilir.
Client Error
İsteğin hatalı olduğunu gösterir. Eksik alan, yanlış token, geçersiz müşteri numarası gibi durumlarda retry genellikle doğru çözüm değildir.
Server Error
Karşı servisin isteği işlerken sorun yaşadığını gösterir. 502, 503 ve 504 gibi hatalar geçici olabilir; bu yüzden sınırlı ve kontrollü retry düşünülebilir.
Transaction Boundary
Database transaction'ının başladığı ve bittiği sınırdır. External API çağrısı bu sınırın içinde yapılırsa DB connection ve lock süreleri gereksiz yere uzayabilir.
Fallback
External servis cevap vermezse uygulanacak alternatif davranıştır. Her servis için fallback doğru değildir; fraud servisi yoksa transferi devam ettirmek yerine durdurmak daha doğru olabilir.
Circuit Breaker
Sürekli hata veren bir servise bir süre istek göndermeyerek sistemi koruma yaklaşımıdır. Bu modülde derin uygulamayacağız ama retry'nin tek başına yeterli olmadığını anlamak için bilmek önemlidir.
Observability
External çağrının ne kadar sürdüğünü, kaç kez hata aldığını ve hangi request ile ilişkili olduğunu görebilme yeteneğidir. Bir sonraki modülde bu konu Actuator ve health check ile birleşecek.
External API çağrısı neden ayrı bir konu?
Çünkü external API çağrısında artık sadece kendi kodumuzun doğruluğuna güvenmeyiz; network, karşı servis, authentication, latency ve hata formatı da iş akışının parçası olur.
Bir Java method'u çağırdığımızda her şey aynı process içinde gerçekleşir. Method yavaşsa profiler ile bakabiliriz, exception atarsa stack trace görürüz, refactor etmek istersek kod bizdedir. External API çağrısında ise çağırdığımız sistem çoğu zaman başka bir ekip, başka bir deployment, başka bir network ve başka bir release takvimi tarafından yönetilir. Bu yüzden external API çağrısı uygulama içinde küçük bir satır gibi görünse de aslında sistemler arası bir sınırdır.
Senior geliştirici için kritik fark şudur: external servis çağrısı başarılı olduğunda değil, başarısız olduğunda tasarımın kalitesi ortaya çıkar. Karşı servis hiç cevap vermezse ne olur? 2 saniye mi bekleriz, 30 saniye mi? 503 geldiğinde tekrar dener miyiz? 400 geldiğinde kullanıcıya ne döneriz? Bu çağrı sırasında DB transaction açık mı kalır? Log'da hangi correlation id ile takip ederiz? Bu sorular net değilse client kodu production'da kırılgan hale gelir.
External API çağrısını “bir servis çağırdım ve cevap aldım” seviyesinde bırakmayacağız. Bu modülde çağrının zaman sınırını, hata sınırını, retry kararını, configuration modelini ve transaction sınırını birlikte düşüneceğiz.
RestTemplate / WebClient kullanımı
Spring tarafında external HTTP çağrısı için en sık iki isimle karşılaşırız: RestTemplate ve WebClient. İkisi de HTTP çağrısı yapar; fakat tasarım dili, konfigürasyon esnekliği ve modern Spring ekosistemindeki konumları farklıdır.
RestTemplate klasik blocking modelle çalışır. Bir request gönderilir ve o request'in cevabı gelene kadar çağıran thread bekler. Bu yaklaşım özellikle eski Spring projelerinde çok yaygındır ve okunması kolaydır. Ancak yeni projelerde WebClient daha modern bir seçenek olarak öne çıkar. WebClient reactive altyapıdan gelir; ama bu, onu sadece WebFlux projelerinde kullanabiliriz demek değildir. Spring MVC uygulamasında da external client yazarken WebClient kullanılabilir.
RestTemplate
Öğrenmesi kolay, imperative ve blocking çalışan klasik HTTP client yaklaşımıdır.
- Legacy projelerde sık görülür.
- Basit çağrılarda okunması kolaydır.
- Thread cevap gelene kadar bekler.
- Modern Spring projelerinde yeni geliştirme için ilk tercih olmayabilir.
WebClient
Daha modern, fluent API sunan ve timeout/error/filter gibi client davranışlarını daha temiz ifade etmeye imkan veren yaklaşımdır.
- MVC uygulamasında da external client olarak kullanılabilir.
- Base URL, header, filter ve error mapping için güçlüdür.
- Reactive akış destekler; gerekirse
block()ile klasik akışa bağlanabilir. - Bu eğitimde MiniBank external client'ları için bunu tercih edeceğiz.
MiniBank için WebClient tabanlı bir client yazacağız. Amacımız reactive programlama öğretmek değil; external API çağrısında merkezi configuration, timeout, retry, error mapping ve transaction boundary farkındalığını temiz bir kod üzerinden göstermek.
// URL kodun içine gömülü, timeout yok, hata ayrımı yok
FraudResponse response = restTemplate.postForObject(
"http://fraud-service/fraud/check",
request,
FraudResponse.class
);
Bu kod demo ortamında çalışabilir; ama production için eksikleri çoktur. URL değişirse kod değişir, karşı servis cevap vermezse ne kadar bekleneceği belirsizdir, 4xx ve 5xx aynı şekilde ele alınabilir ve domain servis bu teknik detaylarla kirlenebilir. Bizim hedefimiz bu çağrıyı ayrı bir client sınıfına almak ve davranışını configuration ile yönetmektir.
HTTP client configuration
External servis URL'ini, timeout değerini, retry sayısını ve aktif/pasif davranışını kodun içine gömmeyiz. Bunlar ortamdan ortama değişebilen configuration değerleridir.
Dev ortamında fraud servisi bir WireMock veya lokal mock olabilir. Test ortamında sandbox endpoint kullanılabilir. Production'da gerçek fraud servisine gidilir. Aynı şekilde timeout değerleri de ortam ve servis kritikliğine göre değişebilir. Kodun içine http://localhost:9090 yazmak, bugünün hızlı çözümünü yarının deployment problemine dönüştürür.
Spring Boot'ta bu değerleri yönetmenin en temiz yollarından biri @ConfigurationProperties kullanmaktır. Böylece configuration string string okunmaz; tipli, gruplanmış ve test edilebilir bir modele dönüşür.
minibank:
integrations:
fraud:
enabled: true
base-url: http://localhost:9090
connect-timeout-ms: 1000
response-timeout-ms: 2000
retry-count: 2
@ConfigurationProperties(prefix = "minibank.integrations.fraud")
public record FraudClientProperties(
boolean enabled,
String baseUrl,
int connectTimeoutMs,
int responseTimeoutMs,
int retryCount
) { }
Doğru yönde ilerleyen tasarım
- URL environment bazlı değişebilir.
- Timeout değerleri kod değişmeden ayarlanır.
- Client davranışı tek yerde okunur.
- Testte fake endpoint kullanmak kolaylaşır.
Kaçınacağımız tasarım
- URL'in servis içinde hard-code edilmesi.
- Timeout değerinin hiç tanımlanmaması.
- Her client'ın kendi başına farklı error handling yapması.
- Prod secret veya token bilgisinin repository'ye yazılması.
Timeout set etme
Timeout, external API çağrısının en temel güvenlik kemeridir. Timeout yoksa sistemin ne kadar bekleyeceğini karşı servis belirler; bu production için kabul edilebilir bir durum değildir.
Timeout sadece “kullanıcı çok beklemesin” konusu değildir. Bir HTTP isteği işlendiği sırada thread, memory, connection ve bazen DB transaction kaynakları kullanılır. Karşı servis cevap vermezse bu kaynaklar beklemeye devam eder. Trafik arttığında birkaç yavaş external çağrı, bütün uygulamanın cevap veremez hale gelmesine yol açabilir.
Connect timeout
Karşı servise bağlantı kurmak için beklenecek maksimum süredir. Servise hiç ulaşılamıyorsa bu sınır devreye girer.
Response timeout
Bağlantı kurulduktan sonra cevabın gelmesi için beklenecek maksimum süredir. Servis çalışıyor ama yavaşsa önemlidir.
Read timeout
Cevap okunurken veri akışının ne kadar sürede tamamlanacağını sınırlar. Büyük response veya network kopması senaryolarında önemlidir.
@Bean
WebClient fraudWebClient(FraudClientProperties properties) {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, properties.connectTimeoutMs())
.responseTimeout(Duration.ofMillis(properties.responseTimeoutMs()));
return WebClient.builder()
.baseUrl(properties.baseUrl())
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
Timeout değerini rastgele seçmeyiz. Çok düşük timeout false failure üretir; çok yüksek timeout sistemi yavaşlatır. Servisin SLA'i, kullanıcı beklentisi, retry sayısı ve toplam request süresi birlikte düşünülmelidir.
Basic retry
Retry, geçici hatalarda işe yarayabilir; fakat yanlış kullanılırsa problemi çözmek yerine büyütür.
Bir external servis 503 döndüğünde birkaç yüz milisaniye sonra tekrar denemek mantıklı olabilir. Çünkü servis kısa süreli yoğunluk yaşıyor olabilir. Fakat istek 400 Bad Request dönüyorsa aynı isteği tekrar göndermek sonucu değiştirmez. Hatalı IBAN, eksik token veya geçersiz müşteri numarası retry ile düzelmez.
Retry kararında bir diğer kritik konu idempotency'dir. Bir bakiye sorgulama isteğini tekrar etmek genellikle güvenlidir. Ama para transferi isteği idempotent tasarlanmamışsa aynı request'in tekrar gönderilmesi çift işlem riski doğurabilir. Bu yüzden retry sadece teknik değil, domain kararıdır.
Retry edilmemesi gerekenler
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- Validation hataları
- İdempotent olmayan yazma operasyonları
Retry düşünülebilecekler
- 502 Bad Gateway
- 503 Service Unavailable
- 504 Gateway Timeout
- Geçici network hataları
- Okuma ağırlıklı idempotent çağrılar
return fraudWebClient.post()
.uri("/fraud/check")
.bodyValue(request)
.retrieve()
.bodyToMono(FraudCheckResult.class)
.timeout(Duration.ofMillis(properties.responseTimeoutMs()))
.retryWhen(Retry.fixedDelay(properties.retryCount(), Duration.ofMillis(200))
.filter(this::isRetryableFailure))
.block();
Karşı servis zaten zorlanıyorken bütün client'lar aynı anda retry yaparsa hata büyür. Bu yüzden gerçek sistemlerde retry çoğu zaman backoff, jitter ve circuit breaker gibi mekanizmalarla birlikte düşünülür. Bu modülde temel retry farkındalığını kuracağız; daha gelişmiş resilience konuları ayrı bir başlık olabilir.
Error mapping: teknik hatayı domain diline çevirmek
External servis hatasını doğrudan controller'a veya kullanıcıya taşımayız. Client katmanı teknik cevabı anlamlandırır ve uygulamanın anlayacağı exception'a çevirir.
Fraud servisi 404, 409, 500 veya timeout dönebilir. Bu cevapların hepsi MiniBank domain'i için aynı anlama gelmez. Bazıları “istek geçersiz”, bazıları “servis geçici olarak kullanılamıyor”, bazıları “risk değerlendirmesi yapılamadı” anlamına gelebilir. Eğer client bu ayrımı yapmazsa service katmanı HTTP detaylarını bilmek zorunda kalır.
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, response ->
Mono.error(new FraudRequestRejectedException("Fraud request is not valid")))
.onStatus(HttpStatusCode::is5xxServerError, response ->
Mono.error(new FraudServiceUnavailableException("Fraud service is temporarily unavailable")))
Controller ve business service mümkün olduğunca “HTTP 503 aldım” diliyle değil, “fraud servisi geçici olarak kullanılamıyor” diliyle çalışmalıdır. Böylece teknik detaylar client katmanında kalır.
External call transaction içinde mi olmalı?
Çoğu durumda external API çağrısını DB transaction'ı açıkken yapmak istemeyiz. Çünkü network beklerken database kaynaklarını da bekletmiş oluruz.
Bir transaction başladığında uygulama database connection alır ve bazı işlemlerde lock oluşabilir. Bu sırada external servis çağrısı yaparsak, karşı servisin yavaşlığı bizim database kaynaklarımızı da gereksiz yere meşgul eder. Transfer gibi kritik bir akışta doğru tasarım genellikle şudur: external karar çağrılarını transaction başlamadan yap, transaction içinde sadece veritabanı tutarlılığı gerektiren kısa bölümü çalıştır.
public void transfer(TransferRequest request) {
fraudCheckClient.check(request.fromIban(), request.toIban(), request.amount());
transferTransactionService.executeTransfer(request);
}
@Transactional
public void executeTransfer(TransferRequest request) {
// Sadece DB tutarlılığı gerektiren kısa bölüm burada çalışır.
}
Transfer öncesi fraud check çağrısını transaction başlamadan yapacağız. Transaction içinde sadece hesap bakiyesi güncelleme, transaction history yazma ve audit gibi DB tutarlılığı gerektiren işleri kısa tutacağız.
MiniBank Case 10 — Fraud Check Client + Timeout + Retry
Bu case'de MiniBank transfer akışına external Fraud Check servisi ekliyoruz. Ama bunu sadece “HTTP çağrısı atalım” şeklinde değil; configuration, timeout, retry, error mapping ve transaction sınırı düşünülmüş bir client olarak yazıyoruz. Görev listesi, doğrulama komutları, kod adımları ve kabul kriterleri Lab Dashboard'da.
Lab sırasında geride kalırsan lab-10-complete branch'ine geçip herkesle aynı checkpoint'ten devam edebilirsin.
Bu modülden akılda kalması gerekenler
Kendi kodundan çıkıp network, karşı servis ve başka bir ekibin lifecycle'ına bağımlı hale gelirsin.
Timeout olmadan thread ve connection kaynakları gereksiz yere bekleyebilir.
4xx, 5xx, timeout ve network hataları aynı değildir; idempotency mutlaka düşünülmelidir.
URL, timeout, retry ve enabled gibi değerler ortam bazlı değişebilir.
Network beklerken DB transaction açık kalırsa connection pool ve lock baskısı oluşabilir.
Business service HTTP detaylarıyla değil, domain'e anlamlı exception'larla çalışmalıdır.
6 kısa kontrol sorusu
1. External API çağrısı neden basit bir method çağrısı gibi düşünülmemelidir?
Çünkü network, karşı servisin durumu, latency, authentication, timeout ve hata formatı gibi bizim uygulamamız dışındaki faktörlere bağlıdır.
2. Timeout neden sadece kullanıcı deneyimi konusu değildir?
Timeout yoksa thread, connection ve bazen transaction kaynakları uzun süre bekleyebilir. Bu da uygulamanın genel dayanıklılığını etkiler.
3. 400 Bad Request için retry yapmak neden genellikle yanlıştır?
Çünkü istek hatalıdır. Aynı hatalı isteği tekrar göndermek sonucu değiştirmez, sadece gereksiz trafik üretir.
4. WebClient kullanmak zorunlu olarak reactive uygulama yazmak anlamına mı gelir?
Hayır. WebClient reactive altyapıdan gelir ama Spring MVC uygulamasında da external client olarak kullanılabilir.
5. External API çağrısını transaction içinde yapmak neden risklidir?
Karşı servis beklerken DB connection ve olası lock'lar açık kalabilir. Bu da connection pool baskısı ve performans problemi yaratabilir.
6. Error mapping neden client katmanında yapılmalıdır?
Çünkü business service HTTP status code gibi teknik detaylarla kirlenmemelidir. Client, teknik cevabı uygulamanın anlayacağı exception modeline çevirmelidir.