Gün 3 / Production Basics / Modül 10
Ana Portal Case'e Git
Modül 10 Gün 3 Production Basics ~80 dk

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.

Anlatım
50 dk
MiniBank Case
48 dk
Branch
lab-10
Odak
Client Design

Ö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.

API

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.

CLI

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.

DTO

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.

TO

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.

RT

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.

ID

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.

4xx

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.

5xx

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.

TX

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.

FB

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.

CB

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.

OBS

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.

01
Request hazırlanır
URL, path, query param, header, body ve authentication bilgileri oluşturulur.
02
Network'e çıkılır
DNS, TCP bağlantısı, TLS handshake ve proxy gibi uygulama dışı adımlar devreye girer.
03
Karşı servis işler
Servis yoğun, yavaş, bakımda veya hatalı sürümde olabilir.
04
Response yorumlanır
2xx, 4xx, 5xx, timeout ve network hataları farklı anlamlara gelir.
05
Domain kararı verilir
MiniBank transferi devam ettirir, durdurur, audit atar veya kontrollü hata döner.
Production farkındalığı

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.
Bu eğitimde kararımız

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.

Kötü başlangıç — hızlı ama kırılgan
// 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.

application.yml
minibank:
  integrations:
    fraud:
      enabled: true
      base-url: http://localhost:9090
      connect-timeout-ms: 1000
      response-timeout-ms: 2000
      retry-count: 2
FraudClientProperties.java
@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.

WebClient configuration — timeout fikri
@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();
}
Senior dikkat noktası

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
Retry fikri — sınırlı ve seçici olmalı
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();
Retry storm riski

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.

FraudCheckClient — error mapping yaklaşımı
.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")))
Tasarım mesajı

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.

01
Yanlış akış
Transaction başlar, DB connection alınır.
02
Network beklenir
Fraud servisi yavaşsa transaction açık kalır.
03
Kaynaklar tutulur
Connection ve lock süreleri uzar.
04
Baskı oluşur
Yoğun trafikte connection pool problemi görülebilir.
Daha kontrollü akış
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.
}
MiniBank kararı

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.

Başlangıç
lab-10-start
Tamamlanmış
lab-10-complete
Checkpoint kuralı

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

1
External API çağrısı sistem sınırıdır.
Kendi kodundan çıkıp network, karşı servis ve başka bir ekibin lifecycle'ına bağımlı hale gelirsin.
2
Timeout opsiyonel değil, temel korumadır.
Timeout olmadan thread ve connection kaynakları gereksiz yere bekleyebilir.
3
Retry bilinçli uygulanmalıdır.
4xx, 5xx, timeout ve network hataları aynı değildir; idempotency mutlaka düşünülmelidir.
4
Client configuration koddan ayrılmalıdır.
URL, timeout, retry ve enabled gibi değerler ortam bazlı değişebilir.
5
External call transaction sınırını etkiler.
Network beklerken DB transaction açık kalırsa connection pool ve lock baskısı oluşabilir.
6
Error mapping client katmanında yapılmalıdır.
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.