← Kursa Dön
📄 Text · 20 min

RestClient & WebClient

Giriş — HTTP Client Evrimi

Spring uygulamaların neredeyse hepsi dış servislerle konuşur. Ödeme API'si, bildirim servisi, başka bir microservice... Bunların hepsi HTTP çağrısı demek. Spring bu iş için yıllar boyunca farklı araçlar sundu:

  • RestTemplate (2009): İlk ve en eski. Senkron, bloklayıcı.

  • WebClient (2017, Spring 5): Reactive, non-blocking. Güçlü ama öğrenme eğrisi dik.

  • RestClient (2023, Spring Boot 3.2): Modern, fluent API. RestTemplate'in halefi.

Bu derste üçünü de öğrenecek, ne zaman hangisini kullanacağını anlayacak ve production-ready HTTP çağrıları yazmayı göreceksin.


1. RestTemplate: Neden Maintenance Mode'da?

RestTemplate uzun yıllar Spring'in varsayılan HTTP client'ıydı. Hâlâ çalışır, hâlâ kullanılabilir. Ama Spring ekibi artık yeni özellik eklemiyor — sadece bug fix.

RestTemplate'in Limitasyonları

// Klasik RestTemplate kullanımı
RestTemplate restTemplate = new RestTemplate();

// GET — basit ama şık değil
ResponseEntity<User> response = restTemplate.getForEntity(
    "https://api.example.com/users/{id}", User.class, 42);
User user = response.getBody();

// POST — verbose, okunabilirlik düşük
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);

HttpEntity<CreateUserRequest> request = new HttpEntity<>(body, headers);
ResponseEntity<User> result = restTemplate.postForEntity(
    "https://api.example.com/users", request, User.class);

Sorunlar:

  1. Senkron ve bloklayıcı — Thread, cevap gelene kadar bekler. Yüksek trafikte thread pool tükenir.

  2. Verbose API — Header eklemek, body ayarlamak çok fazla boilerplate.

  3. Hata yönetimi garip — 4xx/5xx response'larda exception fırlatır (HttpClientErrorException). Kontrol etmek zor.

  4. Fluent API yok — Method chaining desteklemez, konfigürasyon dağınık.

  5. Modern özellikler eksik — Streaming, reactive support yok.

RestTemplate'den Göç

RestTemplate (maintenance) 
    ├── Senkron ihtiyaç → RestClient'a geç (Spring Boot 3.2+)
    └── Reactive/async ihtiyaç → WebClient'a geç

⚠️ Dikkat: RestTemplate "deprecated" değil, "maintenance mode"da. Mevcut kodun çalışmaya devam eder. Ama yeni proje başlıyorsan RestTemplate kullanma. RestClient veya WebClient seç.


2. RestClient: Modern, Fluent API (Spring Boot 3.2+)

RestClient, Spring Framework 6.1 (Spring Boot 3.2) ile geldi. RestTemplate'in modern, fluent versiyonu. Senkron çalışır ama API'si çok temiz.

Temel Kullanım

// RestClient oluşturma
RestClient restClient = RestClient.builder()
    .baseUrl("https://api.example.com")
    .defaultHeader("Accept", "application/json")
    .build();

// Veya basit oluşturma
RestClient restClient = RestClient.create("https://api.example.com");

// Mevcut RestTemplate'den oluşturma (migration için)
RestTemplate restTemplate = new RestTemplate();
RestClient restClient = RestClient.create(restTemplate);

GET İsteği

// Basit GET — body olarak al
User user = restClient.get()
    .uri("/users/{id}", 42)
    .retrieve()
    .body(User.class);

// ResponseEntity olarak al (status, headers erişimi)
ResponseEntity<User> response = restClient.get()
    .uri("/users/{id}", 42)
    .retrieve()
    .toEntity(User.class);

System.out.println("Status: " + response.getStatusCode());
System.out.println("User: " + response.getBody());

// Liste olarak al
List<User> users = restClient.get()
    .uri("/users")
    .retrieve()
    .body(new ParameterizedTypeReference<List<User>>() {});

POST İsteği

// JSON body ile POST
CreateUserRequest request = new CreateUserRequest("Ali", "ali@test.com");

User createdUser = restClient.post()
    .uri("/users")
    .contentType(MediaType.APPLICATION_JSON)
    .body(request)
    .retrieve()
    .body(User.class);

PUT İsteği

UpdateUserRequest updateRequest = new UpdateUserRequest("Ali Yılmaz", "ali@test.com");

User updatedUser = restClient.put()
    .uri("/users/{id}", 42)
    .contentType(MediaType.APPLICATION_JSON)
    .body(updateRequest)
    .retrieve()
    .body(User.class);

DELETE İsteği

// Basit DELETE
restClient.delete()
    .uri("/users/{id}", 42)
    .retrieve()
    .toBodilessEntity();  // Body beklemiyoruz

// DELETE ile response body
ResponseEntity<Void> response = restClient.delete()
    .uri("/users/{id}", 42)
    .retrieve()
    .toBodilessEntity();

System.out.println("Silindi: " + response.getStatusCode());

PATCH İsteği

Map<String, String> patchData = Map.of("email", "yeni@test.com");

User patchedUser = restClient.patch()
    .uri("/users/{id}", 42)
    .contentType(MediaType.APPLICATION_JSON)
    .body(patchData)
    .retrieve()
    .body(User.class);

💡 İpucu: RestClient metod zinciri her zaman aynı sırada: method().uri().headers() / .body().retrieve().body() / .toEntity(). Bu tutarlılık kodu çok okunabilir yapar.

Error Handling

RestClient'ın hata yönetimi RestTemplate'ten çok daha iyi:

// Varsayılan davranış: 4xx/5xx → RestClientResponseException fırlatır
// Bunu özelleştirebilirsin:

User user = restClient.get()
    .uri("/users/{id}", 42)
    .retrieve()
    .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
        // 4xx hatalarında custom işlem
        throw new UserNotFoundException("Kullanıcı bulunamadı: " + 
            new String(response.getBody().readAllBytes()));
    })
    .onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
        // 5xx hatalarında
        throw new ExternalServiceException("Servis hatası: " + 
            response.getStatusCode());
    })
    .body(User.class);
// Spesifik status code kontrolü
User user = restClient.get()
    .uri("/users/{id}", 42)
    .retrieve()
    .onStatus(status -> status.value() == 404, (request, response) -> {
        throw new UserNotFoundException("Kullanıcı #42 bulunamadı");
    })
    .onStatus(status -> status.value() == 429, (request, response) -> {
        throw new RateLimitExceededException("API rate limit aşıldı");
    })
    .body(User.class);

Request Interceptor

Her isteğe otomatik header eklemek, loglama yapmak veya metrikleri toplamak için:

RestClient restClient = RestClient.builder()
    .baseUrl("https://api.example.com")
    .requestInterceptor((request, body, execution) -> {
        // Her isteğe authentication header ekle
        request.getHeaders().setBearerAuth(getAccessToken());
        
        // Request loglama
        long startTime = System.currentTimeMillis();
        ClientHttpResponse response = execution.execute(request, body);
        long duration = System.currentTimeMillis() - startTime;
        
        System.out.printf("[HTTP] %s %s → %s (%dms)%n",
            request.getMethod(), request.getURI(),
            response.getStatusCode(), duration);
        
        return response;
    })
    .build();

Timeout Konfigürasyonu

// Apache HttpClient ile custom timeout
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.core5.util.Timeout;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;

RequestConfig requestConfig = RequestConfig.custom()
    .setConnectionRequestTimeout(Timeout.ofSeconds(5))  // Connection pool'dan almak
    .setResponseTimeout(Timeout.ofSeconds(10))            // Response beklemek
    .build();

var httpClient = HttpClients.custom()
    .setDefaultRequestConfig(requestConfig)
    .build();

var requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);

RestClient restClient = RestClient.builder()
    .baseUrl("https://api.example.com")
    .requestFactory(requestFactory)
    .build();
<!-- Apache HttpClient 5 bağımlılığı -->
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
</dependency>

JDK HttpClient ile (Bağımlılık Yok)

import org.springframework.http.client.JdkClientHttpRequestFactory;
import java.net.http.HttpClient;
import java.time.Duration;

HttpClient jdkClient = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(5))
    .build();

var requestFactory = new JdkClientHttpRequestFactory(jdkClient);
requestFactory.setReadTimeout(Duration.ofSeconds(10));

RestClient restClient = RestClient.builder()
    .baseUrl("https://api.example.com")
    .requestFactory(requestFactory)
    .build();

Header Yönetimi ve Authentication

// Builder'da default header'lar
RestClient restClient = RestClient.builder()
    .baseUrl("https://api.example.com")
    .defaultHeader("X-API-Key", "my-secret-key")
    .defaultHeader("Accept", "application/json")
    .defaultHeader("User-Agent", "MyApp/1.0")
    .build();

// İstek bazında header
User user = restClient.get()
    .uri("/users/{id}", 42)
    .header("X-Request-ID", UUID.randomUUID().toString())
    .header("Accept-Language", "tr")
    .retrieve()
    .body(User.class);

// Bearer token ile
User user = restClient.get()
    .uri("/users/me")
    .headers(headers -> headers.setBearerAuth(jwtToken))
    .retrieve()
    .body(User.class);

// Basic Auth
User user = restClient.get()
    .uri("/admin/users")
    .headers(headers -> headers.setBasicAuth("admin", "password"))
    .retrieve()
    .body(User.class);

Spring Bean Olarak Tanımlama

@Configuration
public class HttpClientConfig {

    @Bean
    public RestClient userServiceClient() {
        return RestClient.builder()
            .baseUrl("https://user-service.internal:8080")
            .defaultHeader("X-Service-Name", "order-service")
            .requestInterceptor(authInterceptor())
            .build();
    }

    @Bean
    public RestClient paymentServiceClient() {
        return RestClient.builder()
            .baseUrl("https://payment-service.internal:8080")
            .defaultHeader("X-Service-Name", "order-service")
            .requestInterceptor(authInterceptor())
            .build();
    }

    private ClientHttpRequestInterceptor authInterceptor() {
        return (request, body, execution) -> {
            request.getHeaders().setBearerAuth(getServiceToken());
            return execution.execute(request, body);
        };
    }
    
    private String getServiceToken() {
        // Service-to-service token (OAuth2 client credentials vb.)
        return "service-token";
    }
}
// Kullanım — Qualifier ile
@Service
public class OrderService {

    private final RestClient userServiceClient;
    private final RestClient paymentServiceClient;

    public OrderService(
            @Qualifier("userServiceClient") RestClient userServiceClient,
            @Qualifier("paymentServiceClient") RestClient paymentServiceClient) {
        this.userServiceClient = userServiceClient;
        this.paymentServiceClient = paymentServiceClient;
    }

    public User getUser(Long userId) {
        return userServiceClient.get()
            .uri("/api/users/{id}", userId)
            .retrieve()
            .body(User.class);
    }

    public PaymentResult processPayment(PaymentRequest request) {
        return paymentServiceClient.post()
            .uri("/api/payments")
            .body(request)
            .retrieve()
            .body(PaymentResult.class);
    }
}

💡 İpucu: Her dış servis için ayrı RestClient bean'i oluştur. Her birinin kendi base URL'i, timeout'u, authentication'ı olur. Bu separation of concerns, microservices mimarisinde çok önemli.


3. WebClient: Non-Blocking, Reactive

WebClient, Spring WebFlux ile birlikte gelir. Non-blocking, reactive bir HTTP client'tır. Yüksek concurrency gerektiren senaryolarda thread'leri bloklamadan binlerce eşzamanlı HTTP çağrısı yapabilirsin.

Bağımlılık

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

💡 İpucu: spring-boot-starter-webflux eklemen, uygulamanı reactive yapmaz. Spring MVC ile birlikte kullanabilirsin. WebClient'ı sadece HTTP client olarak kullanıp, uygulamanı servlet stack'te tutabilirsin.

Temel Kullanım

// WebClient oluşturma
WebClient webClient = WebClient.builder()
    .baseUrl("https://api.example.com")
    .defaultHeader("Accept", "application/json")
    .build();

// Basit oluşturma
WebClient webClient = WebClient.create("https://api.example.com");

GET İsteği

// Mono — tek bir sonuç (0 veya 1)
Mono<User> userMono = webClient.get()
    .uri("/users/{id}", 42)
    .retrieve()
    .bodyToMono(User.class);

// Bloklama ile senkron kullanım (önerilmez ama geçiş döneminde faydalı)
User user = userMono.block();

// Reactive kullanım — subscribe et
userMono.subscribe(
    user -> System.out.println("Kullanıcı: " + user.getName()),
    error -> System.err.println("Hata: " + error.getMessage())
);

// Flux — liste (0..N)
Flux<User> usersFlux = webClient.get()
    .uri("/users")
    .retrieve()
    .bodyToFlux(User.class);

// Liste olarak al
List<User> users = usersFlux.collectList().block();

POST İsteği

// POST — body ile
Mono<User> createdUser = webClient.post()
    .uri("/users")
    .contentType(MediaType.APPLICATION_JSON)
    .bodyValue(new CreateUserRequest("Ali", "ali@test.com"))
    .retrieve()
    .bodyToMono(User.class);

// Reactive chain'de kullanım
createdUser
    .doOnSuccess(user -> log.info("Kullanıcı oluşturuldu: {}", user.getId()))
    .doOnError(error -> log.error("Hata: {}", error.getMessage()))
    .subscribe();

PUT ve DELETE

// PUT
Mono<User> updated = webClient.put()
    .uri("/users/{id}", 42)
    .bodyValue(updateRequest)
    .retrieve()
    .bodyToMono(User.class);

// DELETE
Mono<Void> deleted = webClient.delete()
    .uri("/users/{id}", 42)
    .retrieve()
    .bodyToMono(Void.class);

Error Handling

// onStatus ile hata kontrolü
Mono<User> user = webClient.get()
    .uri("/users/{id}", 42)
    .retrieve()
    .onStatus(HttpStatusCode::is4xxClientError, response -> {
        return response.bodyToMono(String.class)
            .flatMap(body -> Mono.error(
                new UserNotFoundException("Kullanıcı bulunamadı: " + body)));
    })
    .onStatus(HttpStatusCode::is5xxServerError, response -> {
        return Mono.error(new ExternalServiceException("Servis hatası"));
    })
    .bodyToMono(User.class);
// onErrorResume — hata durumunda fallback
Mono<User> userWithFallback = webClient.get()
    .uri("/users/{id}", 42)
    .retrieve()
    .bodyToMono(User.class)
    .onErrorResume(WebClientResponseException.NotFound.class, ex -> {
        // 404 durumunda varsayılan kullanıcı döndür
        return Mono.just(User.defaultUser());
    })
    .onErrorResume(Exception.class, ex -> {
        // Diğer hatalarda cache'ten al
        return getUserFromCache(42);
    });

Retry Mekanizması

import reactor.util.retry.Retry;

// Basit retry
Mono<User> user = webClient.get()
    .uri("/users/{id}", 42)
    .retrieve()
    .bodyToMono(User.class)
    .retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(1)));
    // 3 kez dene, aralarında 1 saniye bekle

// Exponential backoff ile retry
Mono<User> user = webClient.get()
    .uri("/users/{id}", 42)
    .retrieve()
    .bodyToMono(User.class)
    .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
        .maxBackoff(Duration.ofSeconds(10))       // Max bekleme süresi
        .jitter(0.5)                               // Rastgele jitter
        .filter(ex -> ex instanceof WebClientResponseException.ServiceUnavailable)
        .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> 
            new ExternalServiceException(
                "Servis yanıt vermiyor. " + retrySignal.totalRetries() + " deneme yapıldı.",
                retrySignal.failure()
            ))
    );

Timeout Konfigürasyonu

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import reactor.netty.http.client.HttpClient;

HttpClient httpClient = HttpClient.create()
    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)  // Connection timeout: 5s
    .doOnConnected(conn -> conn
        .addHandlerLast(new ReadTimeoutHandler(10))       // Read timeout: 10s
        .addHandlerLast(new WriteTimeoutHandler(10)));     // Write timeout: 10s

WebClient webClient = WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .baseUrl("https://api.example.com")
    .build();

// İstek bazında timeout
Mono<User> user = webClient.get()
    .uri("/users/{id}", 42)
    .retrieve()
    .bodyToMono(User.class)
    .timeout(Duration.ofSeconds(5));  // Toplam 5 saniye timeout

Exchange Filter Functions

WebClient'ın interceptor'ları — her isteği ve yanıtı değiştirebilirsin:

// Loglama filter'ı
ExchangeFilterFunction logFilter = ExchangeFilterFunction.ofRequestProcessor(request -> {
    System.out.printf("[→] %s %s%n", request.method(), request.url());
    return Mono.just(request);
});

ExchangeFilterFunction logResponseFilter = ExchangeFilterFunction.ofResponseProcessor(response -> {
    System.out.printf("[←] Status: %s%n", response.statusCode());
    return Mono.just(response);
});

// Authentication filter
ExchangeFilterFunction authFilter = (request, next) -> {
    ClientRequest newRequest = ClientRequest.from(request)
        .header("Authorization", "Bearer " + getToken())
        .build();
    return next.exchange(newRequest);
};

// Hepsini birleştir
WebClient webClient = WebClient.builder()
    .baseUrl("https://api.example.com")
    .filter(authFilter)
    .filter(logFilter)
    .filter(logResponseFilter)
    .build();

WebClient'ı Senkron Kullanma (Spring MVC'de)

Reactive stack kullanmıyorsan ama WebClient'ın güzel API'sini istiyorsan:

@Service
public class UserService {

    private final WebClient webClient;

    public UserService(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder
            .baseUrl("https://api.example.com")
            .build();
    }

    // .block() ile senkron kullanım
    public User getUser(Long id) {
        return webClient.get()
            .uri("/users/{id}", id)
            .retrieve()
            .bodyToMono(User.class)
            .block();  // Bloklayıcı — reactive olmayan ortamda
    }

    public List<User> getAllUsers() {
        return webClient.get()
            .uri("/users")
            .retrieve()
            .bodyToFlux(User.class)
            .collectList()
            .block();
    }
}

⚠️ Dikkat: .block() çağrısını reactive pipeline içinde (WebFlux controller, Reactor chain) asla kullanma. Deadlock oluşur. .block() sadece servlet-based (Spring MVC) uygulamalarda, non-reactive context'te güvenlidir.


4. RestClient vs WebClient vs RestTemplate

Karşılaştırma Tablosu

ÖzellikRestTemplateRestClientWebClient
Spring sürümü3.0+ (eski)6.1+ (Boot 3.2)5.0+ (Boot 2.0)
DurumMaintenanceAktif geliştirmeAktif geliştirme
API stiliVerbose, method-basedFluent, builderFluent, builder
ExecutionSenkronSenkronAsync + Senkron
Thread modeliBloklayıcıBloklayıcıNon-blocking
Reactive support✅ (Mono/Flux)
Error handlingException fırlatıronStatus() handleronStatus() handler
StreamingSınırlıSınırlı✅ (Flux)
Interceptor✅ (Filter)
Learning curveDüşükDüşükOrta-Yüksek
Dependencyspring-webspring-webspring-webflux
Connection poolApache HC / OkHttpApache HC / JDKReactor Netty

Ne Zaman Hangisi?

Yeni proje mi?
├── Evet
│   ├── Spring Boot 3.2+ mu?
│   │   ├── Evet
│   │   │   ├── Senkron yeterli mi? → RestClient ✓
│   │   │   └── Reactive/async gerekli mi? → WebClient ✓
│   │   └── Hayır (eski versiyon) → WebClient ✓
│   └── Spring Boot 2.x mi? → WebClient ✓
└── Mevcut proje, RestTemplate var
    ├── Sorun yaşıyor musun?
    │   ├── Hayır → Değiştirme, çalışıyorsa dokunma
    │   └── Evet → RestClient'a migration (Spring Boot 3.2+)
    └── Yeni endpoint mi ekliyorsun? → RestClient kullan

Karar Özeti

  • RestClient: Senkron HTTP çağrıları için varsayılan seçim (Spring Boot 3.2+). Basit, temiz, öğrenmesi kolay.

  • WebClient: Reactive stack, yüksek concurrency, streaming, non-blocking gerektiğinde. Veya Spring Boot 3.2 öncesi.

  • RestTemplate: Sadece legacy kod. Yenisini yazma.

💡 İpucu: RestClient, internally RestTemplate'in HTTP client infrastructure'ını kullanır. Yani performance farkı yok, sadece API farkı var. Geçiş riski minimal.


5. Inter-Service Communication

Microservices mimarisinde servisler arası iletişim kritik. Hangi client'ı kullanacağın, sistemin genel performansını etkiler.

Senkron İletişim

// Order Service → User Service çağrısı
@Service
public class OrderService {

    private final RestClient userServiceClient;
    private final RestClient inventoryServiceClient;

    public OrderService(
            @Qualifier("userServiceClient") RestClient userServiceClient,
            @Qualifier("inventoryServiceClient") RestClient inventoryServiceClient) {
        this.userServiceClient = userServiceClient;
        this.inventoryServiceClient = inventoryServiceClient;
    }

    public OrderResponse createOrder(OrderRequest request) {
        // 1. Kullanıcıyı doğrula
        User user = userServiceClient.get()
            .uri("/api/users/{id}", request.getUserId())
            .retrieve()
            .onStatus(status -> status.value() == 404, (req, resp) -> {
                throw new UserNotFoundException("Kullanıcı bulunamadı");
            })
            .body(User.class);

        // 2. Stok kontrolü
        StockResponse stock = inventoryServiceClient.get()
            .uri("/api/stock/{sku}", request.getProductSku())
            .retrieve()
            .body(StockResponse.class);

        if (stock.getAvailable() < request.getQuantity()) {
            throw new InsufficientStockException("Yetersiz stok");
        }

        // 3. Siparişi oluştur
        Order order = new Order(user, request);
        orderRepository.save(order);

        // 4. Stok düş
        inventoryServiceClient.post()
            .uri("/api/stock/reserve")
            .body(new ReserveRequest(request.getProductSku(), request.getQuantity()))
            .retrieve()
            .toBodilessEntity();

        return OrderResponse.from(order);
    }
}

Paralel Çağrılar (WebClient ile)

Birden fazla servisi aynı anda çağırmak istiyorsan, WebClient ile reactive:

@Service
public class DashboardService {

    private final WebClient userClient;
    private final WebClient orderClient;
    private final WebClient inventoryClient;

    public Mono<DashboardData> getDashboard(Long userId) {
        // Üç servisi paralel çağır
        Mono<User> userMono = userClient.get()
            .uri("/api/users/{id}", userId)
            .retrieve()
            .bodyToMono(User.class);

        Mono<List<Order>> ordersMono = orderClient.get()
            .uri("/api/orders?userId={id}", userId)
            .retrieve()
            .bodyToFlux(Order.class)
            .collectList();

        Mono<Integer> stockCountMono = inventoryClient.get()
            .uri("/api/stock/count")
            .retrieve()
            .bodyToMono(Integer.class);

        // Hepsini birleştir — paralel çalışır!
        return Mono.zip(userMono, ordersMono, stockCountMono)
            .map(tuple -> new DashboardData(
                tuple.getT1(),  // User
                tuple.getT2(),  // Orders
                tuple.getT3()   // Stock count
            ));
    }
}

Bu örnekte üç HTTP çağrısı aynı anda yapılır. Senkron olsa: 200ms + 150ms + 100ms = 450ms beklersin. Paralel olsa: max(200, 150, 100) = 200ms. Büyük fark!

⚠️ Dikkat: Microservices'te senkron çağrılar "cascading failure" riski taşır. User Service çöktüğünde Order Service de çöker. Circuit breaker pattern (Resilience4j) kullan. Bir sonraki bölümde göreceğiz.


6. Retry Pattern ve Exponential Backoff

Dış servisler her zaman erişilebilir değildir. Geçici ağ sorunları, servis restart'ları... Retry pattern bu durumlar için şart.

RestClient ile Retry (Manuel)

@Service
public class ResilientUserService {

    private final RestClient restClient;
    private static final int MAX_RETRIES = 3;

    public User getUserWithRetry(Long userId) {
        int attempt = 0;
        
        while (attempt < MAX_RETRIES) {
            try {
                return restClient.get()
                    .uri("/api/users/{id}", userId)
                    .retrieve()
                    .body(User.class);
            } catch (RestClientException e) {
                attempt++;
                if (attempt >= MAX_RETRIES) {
                    throw new ExternalServiceException(
                        "User servisi yanıt vermiyor. " + attempt + " deneme yapıldı.", e);
                }
                
                // Exponential backoff: 1s, 2s, 4s
                long waitMs = (long) Math.pow(2, attempt - 1) * 1000;
                try {
                    Thread.sleep(waitMs);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException(ie);
                }
            }
        }
        
        throw new ExternalServiceException("Beklenmeyen durum");
    }
}

Spring Retry ile

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>
@Configuration
@EnableRetry
public class RetryConfig {}
@Service
public class UserServiceWithSpringRetry {

    private final RestClient restClient;

    @Retryable(
        retryFor = RestClientException.class,
        maxAttempts = 3,
        backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 10000)
    )
    public User getUser(Long userId) {
        return restClient.get()
            .uri("/api/users/{id}", userId)
            .retrieve()
            .body(User.class);
    }

    @Recover
    public User getUserFallback(RestClientException e, Long userId) {
        // Tüm retry'lar başarısız olursa
        log.warn("User servisi erişilemez, cache'ten dönüyoruz. userId={}", userId);
        return userCacheService.getCachedUser(userId)
            .orElseThrow(() -> new ExternalServiceException(
                "User servisi erişilemez ve cache'te yok", e));
    }
}

WebClient ile Retry (Reactor)

public Mono<User> getUserReactive(Long userId) {
    return webClient.get()
        .uri("/api/users/{id}", userId)
        .retrieve()
        .bodyToMono(User.class)
        .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
            .maxBackoff(Duration.ofSeconds(10))
            .jitter(0.5)
            .filter(this::isRetryable)  // Sadece belirli hataları retry et
            .doBeforeRetry(signal -> 
                log.warn("Retry #{}: {}", signal.totalRetries(), 
                         signal.failure().getMessage()))
        )
        .onErrorResume(ex -> {
            log.error("Tüm retry'lar başarısız", ex);
            return Mono.error(new ExternalServiceException("Servis erişilemez", ex));
        });
}

private boolean isRetryable(Throwable ex) {
    if (ex instanceof WebClientResponseException wcre) {
        // 5xx hatalarını retry et, 4xx hatalarını etme
        return wcre.getStatusCode().is5xxServerError();
    }
    // Connection hataları retry edilebilir
    return ex instanceof ConnectException || 
           ex instanceof TimeoutException;
}

💡 İpucu: Her hatayı retry etme! 400 Bad Request retry edilmemeli — her seferinde aynı sonucu alırsın. 500 Internal Server Error veya connection timeout retry edilebilir. filter() ile retry edilecek hataları sınırla.


7. Gerçek Dünya Örneği: Tam Bir Service Client

Tüm best practice'leri birleştiren production-ready bir service client:

@Configuration
public class PaymentClientConfig {

    @Value("${payment.service.url}")
    private String paymentServiceUrl;

    @Value("${payment.service.api-key}")
    private String apiKey;

    @Bean
    public RestClient paymentClient() {
        // Timeout konfigürasyonu
        HttpClient jdkClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(5))
            .build();
        
        var requestFactory = new JdkClientHttpRequestFactory(jdkClient);
        requestFactory.setReadTimeout(Duration.ofSeconds(10));

        return RestClient.builder()
            .baseUrl(paymentServiceUrl)
            .requestFactory(requestFactory)
            .defaultHeader("X-API-Key", apiKey)
            .defaultHeader("Content-Type", "application/json")
            .defaultHeader("Accept", "application/json")
            .requestInterceptor(loggingInterceptor())
            .defaultStatusHandler(HttpStatusCode::is5xxServerError, 
                (request, response) -> {
                    throw new PaymentServiceUnavailableException(
                        "Payment servisi hatası: " + response.getStatusCode());
                })
            .build();
    }

    private ClientHttpRequestInterceptor loggingInterceptor() {
        return (request, body, execution) -> {
            String requestId = UUID.randomUUID().toString().substring(0, 8);
            request.getHeaders().set("X-Request-ID", requestId);
            
            long start = System.nanoTime();
            try {
                ClientHttpResponse response = execution.execute(request, body);
                long durationMs = (System.nanoTime() - start) / 1_000_000;
                
                log.info("[Payment] {} {} → {} ({}ms) [{}]",
                    request.getMethod(), request.getURI(),
                    response.getStatusCode(), durationMs, requestId);
                
                return response;
            } catch (IOException e) {
                long durationMs = (System.nanoTime() - start) / 1_000_000;
                log.error("[Payment] {} {} → ERROR ({}ms) [{}]: {}",
                    request.getMethod(), request.getURI(),
                    durationMs, requestId, e.getMessage());
                throw e;
            }
        };
    }
}
@Service
public class PaymentService {

    private final RestClient paymentClient;

    public PaymentService(@Qualifier("paymentClient") RestClient paymentClient) {
        this.paymentClient = paymentClient;
    }

    @Retryable(
        retryFor = PaymentServiceUnavailableException.class,
        maxAttempts = 3,
        backoff = @Backoff(delay = 1000, multiplier = 2)
    )
    public PaymentResult charge(PaymentRequest request) {
        return paymentClient.post()
            .uri("/api/v1/charges")
            .body(request)
            .retrieve()
            .onStatus(status -> status.value() == 402, (req, resp) -> {
                String body = new String(resp.getBody().readAllBytes());
                throw new InsufficientFundsException("Yetersiz bakiye: " + body);
            })
            .onStatus(status -> status.value() == 422, (req, resp) -> {
                String body = new String(resp.getBody().readAllBytes());
                throw new InvalidPaymentException("Geçersiz ödeme bilgisi: " + body);
            })
            .body(PaymentResult.class);
    }

    public PaymentStatus getPaymentStatus(String paymentId) {
        return paymentClient.get()
            .uri("/api/v1/charges/{id}/status", paymentId)
            .retrieve()
            .onStatus(status -> status.value() == 404, (req, resp) -> {
                throw new PaymentNotFoundException(
                    "Ödeme bulunamadı: " + paymentId);
            })
            .body(PaymentStatus.class);
    }

    @Retryable(
        retryFor = PaymentServiceUnavailableException.class,
        maxAttempts = 2,
        backoff = @Backoff(delay = 500)
    )
    public RefundResult refund(String paymentId, RefundRequest request) {
        return paymentClient.post()
            .uri("/api/v1/charges/{id}/refunds", paymentId)
            .body(request)
            .retrieve()
            .body(RefundResult.class);
    }

    @Recover
    public PaymentResult chargeFallback(PaymentServiceUnavailableException e, 
                                         PaymentRequest request) {
        log.error("Payment servisi erişilemez, ödeme kuyruğa alınıyor", e);
        // Ödemeyi kuyruğa at, sonra tekrar dene
        paymentQueueService.enqueue(request);
        return PaymentResult.pending("Ödeme işleniyor, birazdan tamamlanacak");
    }
}

Yaygın Hatalar ve Çözümleri

1. .block() Reactive Chain İçinde

// YANLIŞ — Deadlock riski!
@GetMapping("/users/{id}")  // WebFlux controller
public Mono<User> getUser(@PathVariable Long id) {
    User user = webClient.get()
        .uri("/api/users/{id}", id)
        .retrieve()
        .bodyToMono(User.class)
        .block();  // ❌ Reactive context'te block() = DEADLOCK
    return Mono.just(user);
}

// DOĞRU
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id) {
    return webClient.get()
        .uri("/api/users/{id}", id)
        .retrieve()
        .bodyToMono(User.class);  // ✅ Mono'yu doğrudan döndür
}

2. Timeout Ayarlanmamış

// YANLIŞ — Timeout yok, dış servis yanıt vermezse sonsuza kadar bekler
RestClient restClient = RestClient.create("https://slow-api.com");

// DOĞRU — Mutlaka timeout konfigüre et
// (Örnekler yukarıda detaylı gösterildi)

3. Generic Type Erasure

// YANLIŞ — Runtime'da List<User> bilgisi kaybolur
List<User> users = restClient.get()
    .uri("/users")
    .retrieve()
    .body(List.class);  // ❌ List<LinkedHashMap> döner, List<User> değil!

// DOĞRU — ParameterizedTypeReference kullan
List<User> users = restClient.get()
    .uri("/users")
    .retrieve()
    .body(new ParameterizedTypeReference<List<User>>() {});  // ✅

4. RestClient Bean Singleton Thread Safety

// RestClient thread-safe'tir — singleton bean olarak güvenle kullanabilirsin
@Bean
public RestClient apiClient() {
    return RestClient.builder()
        .baseUrl("https://api.example.com")
        .build();
    // ✅ Tüm thread'ler aynı instance'ı paylaşır, sorun yok
}

Özet

  • RestTemplate maintenance mode'da — yeni projede kullanma, mevcut projedeki kodu hemen değiştirmen de gerekmez

  • RestClient (Spring Boot 3.2+), senkron HTTP çağrıları için varsayılan seçim — fluent API, temiz error handling, interceptor desteği, öğrenmesi kolay

  • WebClient reactive/async senaryolar için güçlü — Mono/Flux ile non-blocking, paralel çağrılar, streaming, yüksek concurrency

  • Her dış servis için ayrı RestClient/WebClient bean oluştur — base URL, timeout, auth hepsi ayrı konfigüre edilir

  • Retry pattern production'da zorunlu — @Retryable veya Reactor retryWhen() ile exponential backoff, sadece retryable hataları (5xx, timeout) retry et

  • Timeout mutlaka konfigüre et — varsayılan sınırsız, dış servis yanıt vermezse thread sonsuza kadar bloklanır