Microservices (Java/Spring) · میکروسرویس سنیورSenior ~51 دقیقه مطالعه~45 min read

API Gateway، Service Discovery و ConfigAPI Gateway, Service Discovery & Config

سه ستون عملیاتی میکروسرویس‌ها — API Gateway به‌عنوان دروازهٔ لبه، Service Discovery برای پیدا کردن پویای سرویس‌ها و Config متمرکز برای مدیریت تنظیمات — را از تحلیل تا کد واقعی Spring و درس‌های تولید یاد می‌گیری.Learn the three operational pillars of microservices — the API Gateway as an edge doorway, Service Discovery for finding services dynamically, and centralized Config — from analogy through real Spring code to hard-won production lessons.

پیش‌نیاز:Prerequisites: میکروسرویس: مبانی، مرزبندی و کِی/چراMicroservices: Foundations, Boundaries & When/Why


وقتی از مونولیت به میکروسرویس مهاجرت می‌کنی، سه سؤال جدید و بی‌رحم روی میز می‌آید که در دنیای مونولیت اصلاً وجود نداشتند:

۱. کلاینت باید با چند ده سرویس مختلف حرف بزند؟ آدرس هرکدام را از کجا بداند؟ ۲. سرویس A چطور آدرس سرویس B را پیدا می‌کند وقتی هر لحظه ممکن است ۳ نمونه (instance) از B بالا و پایین شوند؟ ۳. تنظیمات (رشتهٔ اتصال دیتابیس، فلگ‌ها، سقف نرخ) در ۴۰ سرویس چطور مدیریت می‌شوند بدون اینکه ۴۰ بار deploy کنی؟

این فصل دقیقاً همین سه دردِ عملیاتی را جواب می‌دهد: API Gateway (دروازهٔ ورودی)، Service Discovery (دفترچه‌تلفن زندهٔ سرویس‌ها) و Centralized Config (اتاق کنترل تنظیمات). این سه، «سیستم عصبی» یک معماری میکروسرویس هستند. اگر منطق دامنه (business logic) قلب سیستم است، این‌ها رگ‌ها و اعصابی هستند که آن قلب را زنده نگه می‌دارند.

نقشهٔ راه این فصل
  • مسئلهٔ بنیادی: چرا مونولیت این دردها را نداشت و میکروسرویس دارد.
  • Service Discovery: کشف سمت‌کلاینت در برابر سمت‌سرور، Eureka، Consul و Kubernetes DNS.
  • Load Balancing: توزیع بار سمت‌کلاینت با Spring Cloud LoadBalancer (جانشین Ribbon).
  • API Gateway: Spring Cloud Gateway، routeها، predicateها، filterها، rate limiting و احراز هویت در لبه.
  • BFF Pattern: یک gateway به‌ازای هر نوع کلاینت.
  • Config متمرکز: Spring Cloud Config، profileها، refresh زنده و مدیریت رازها (secrets).
  • دید سنیور: تله‌های تولید، anti-patternها و نحوهٔ حرف زدن دربارهٔ همهٔ این‌ها در مصاحبه.

چرا مونولیت این دردها را نداشت؟

در یک مونولیت، وقتی ماژول «سفارش» می‌خواست ماژول «کاربر» را صدا بزند، فقط یک فراخوانی متد ساده در همان JVM بود: userService.findById(id). آدرسی وجود نداشت چون همه‌چیز داخل یک process بود. تنظیمات یک فایل application.yml بود. کلاینت هم فقط یک آدرس می‌شناخت: خودِ مونولیت.

میکروسرویس این راحتی را می‌شکند و به‌جایش مقیاس‌پذیری و استقلال تیم‌ها می‌دهد. اما آن فراخوانی متد ساده حالا یک network call روی TCP است — و شبکه غیرقابل‌اعتماد، کند و متغیر است. آدرس‌ها پویا شده‌اند چون در Kubernetes یک Pod هر لحظه می‌میرد و با IP جدید بالا می‌آید. این سه ابزار دقیقاً برای پر کردن این خلأ ساخته شده‌اند.

فرودگاه به‌جای خانه

مونولیت مثل خانهٔ خودت است: می‌خواهی نمک را برداری، دستت را دراز می‌کنی و برمی‌داری. میکروسرویس مثل یک فرودگاه بین‌المللی است. مسافر (کلاینت) نمی‌تواند مستقیم برود سراغ هر گیت و هر بار. API Gateway آن سالن ورودی و باجهٔ چک‌این است: بلیت را چک می‌کند (auth)، صف را مدیریت می‌کند (rate limit) و تو را به گیت درست هدایت می‌کند. Service Discovery آن تابلوی پروازِ زندهٔ بالای سر است که مدام به‌روز می‌شود و می‌گوید کدام پرواز از کدام گیت. Config Server هم اتاق کنترل فرودگاه است که قوانین را یک‌جا نگه می‌دارد و به همه ابلاغ می‌کند.

نمای کلی توپولوژی را ببین. Bird’s-eye topology of a microservice platform / نمای کلی توپولوژی یک پلتفرم میکروسرویس:

flowchart TD
  Client[Mobile / Web / Partner] --> GW[API Gateway]
  GW -->|discovery| Reg[(Service Registry)]
  GW --> Order[Order Service]
  GW --> User[User Service]
  GW --> Pay[Payment Service]
  Order -. register .-> Reg
  User -. register .-> Reg
  Pay -. register .-> Reg
  Cfg[Config Server] -. config .-> Order
  Cfg -. config .-> User
  Cfg -. config .-> Pay
  Cfg --> Git[(Git repo)]
  Order --> OrderDB[(Order DB)]
  User --> UserDB[(User DB)]

سه بازیگر کلیدی این نقشه — Gateway، Registry و Config Server — موضوع همین فصل‌اند. حالا یکی‌یکی می‌رویم سراغشان، از پایین‌ترین لایه (پیدا کردن سرویس‌ها) به بالاترین (دروازهٔ لبه).


بخش ۱: Service Discovery — دفترچه‌تلفن زندهٔ سرویس‌ها

فرض کن سرویس Order می‌خواهد Payment را صدا بزند. آدرسش چیست؟ اگر بنویسی http://192.168.4.11:8080 فاجعه است: فردا آن Pod می‌میرد، IP عوض می‌شود، سه نمونهٔ جدید بالا می‌آید. هارد-کد کردن آدرس در دنیای پویا معنی ندارد.

دفترچه‌تلفن در برابر شماره‌های حفظی

هارد-کد کردن IP مثل حفظ کردن شمارهٔ موبایل دوستت است. روزی که او شماره‌اش را عوض کند، تو گیر می‌افتی. Service Discovery یعنی به‌جای حفظ شماره، به یک «دفترچه‌تلفن مرکزی» زنگ بزنی و بپرسی: «شمارهٔ فعلیِ سرویس Payment چیست؟» آن دفترچه همیشه به‌روز است چون هر سرویسی وقتی بالا می‌آید خودش را ثبت می‌کند و وقتی می‌میرد حذف می‌شود.

مفهوم: Registry، Registration و Resolution

سه واژه را از صفر بساز:

  • Service Registry: یک پایگاه‌دادهٔ زنده که فهرست «نام سرویس → لیست آدرس‌های سالمِ فعلی» را نگه می‌دارد. مثال: Eureka، Consul، etcd (زیرِ Kubernetes).
  • Registration: عملی که یک instance وقتی بالا می‌آید خودش را با نام و آدرس در registry ثبت می‌کند و بعد به‌طور دوره‌ای «heartbeat» می‌فرستد تا بگوید «هنوز زنده‌ام».
  • Resolution (Discovery): عملی که سرویس مصرف‌کننده، نامِ منطقی (payment-service) را به یک آدرس واقعی ترجمه می‌کند.

کشف سمت‌کلاینت در برابر سمت‌سرور

این تفکیک، یکی از سؤال‌های محبوب مصاحبه است. دو مدل بنیادی وجود دارد:

Client-side discovery: خودِ سرویس مصرف‌کننده مستقیماً از registry لیست آدرس‌ها را می‌گیرد، یکی را انتخاب می‌کند (load balancing) و مستقیم صدا می‌زند. Netflix Eureka + Spring Cloud LoadBalancer دقیقاً این مدل است. مزیت: یک hop شبکه‌ای کمتر و کنترل دقیق روی الگوریتم انتخاب. عیب: هر کلاینت باید منطق discovery و load balancing را در خودش داشته باشد (وابستگی به کتابخانه، و برای چند-زبانه بودن دردسر).

Server-side discovery: کلاینت فقط به یک آدرس ثابت (load balancer یا gateway) می‌زند و آن واسطه از registry استفاده می‌کند تا به instance درست مسیر بدهد. Kubernetes Service دقیقاً همین است: تو به یک نام DNS ثابت می‌زنی و kube-proxy پشت‌صحنه بار را روی Podها پخش می‌کند. مزیت: کلاینت هیچ منطقی نمی‌خواهد. عیب: یک hop و یک نقطهٔ زیرساخت اضافه.

Client-side vs server-side discovery / مقایسهٔ کشف سمت‌کلاینت و سمت‌سرور:

flowchart LR
  subgraph ClientSide[Client-side discovery]
    C1[Consumer] -->|1 ask| R1[(Registry)]
    C1 -->|2 pick & call| P1[Provider instance]
  end
  subgraph ServerSide[Server-side discovery]
    C2[Consumer] -->|call fixed name| LB[Load Balancer / Service]
    LB -->|route| P2[Provider instance]
    LB -.-> R2[(Registry)]
  end
قضاوت سنیور: کدام مدل؟

اگر روی Kubernetes هستی، معمولاً به Eureka احتیاج نداری. خودِ Kubernetes یک registry (etcd) و server-side discovery داخلی (Service + DNS + kube-proxy) دارد. اضافه کردن Eureka روی Kubernetes یعنی دو سیستم discovery موازی که همدیگر را نمی‌شناسند — منبع باگ‌های عجیب. سنیورها Eureka را عمدتاً در محیط‌های غیرِ Kubernetes (مثلاً VM یا Cloud Foundry) یا در سیستم‌های legacy Spring Cloud نگه می‌دارند. جملهٔ طلایی در مصاحبه: «روی k8s، Service Discovery را به پلتفرم می‌سپارم، نه به کتابخانهٔ اپلیکیشن.»

پیاده‌سازی با Eureka (Netflix)

Eureka از پروژهٔ Spring Cloud Netflix می‌آید. یک Eureka Server داری که نقش registry را بازی می‌کند و کلاینت‌ها خودشان را در آن ثبت می‌کنند. پورت پیش‌فرض 8761 است.

سرور:

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
  public static void main(String[] args) {
    SpringApplication.run(DiscoveryServerApplication.class, args);
  }
}
# application.yml — Eureka Server
server:
  port: 8761
eureka:
  client:
    register-with-eureka: false   # خودِ سرور خودش را ثبت نمی‌کند
    fetch-registry: false
  server:
    enable-self-preservation: true   # پیش‌فرض true — مهم! پایین‌تر توضیح می‌دهم

سرویس مصرف‌کننده (client)، فقط با داشتن dependency و آدرس سرور، خودکار ثبت می‌شود:

# application.yml — یک سرویس معمولی
spring:
  application:
    name: payment-service     # همین نام کلید discovery است
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
    lease-renewal-interval-in-seconds: 30    # هر ۳۰ ثانیه heartbeat (پیش‌فرض)
    lease-expiration-duration-in-seconds: 90 # اگر ۹۰ ثانیه heartbeat نیامد، حذف

حالا Order می‌تواند Payment را با نامِ منطقی صدا بزند (نه IP)، به‌شرطِ اینکه یک RestClient/WebClient با load balancing داشته باشد که در بخش بعد می‌سازیم.

تلهٔ کلاسیک تولید: Self-Preservation Mode

Eureka یک مکانیزم دفاعی به نام self-preservation دارد که به‌طور پیش‌فرض روشن است. منطقش این است: «اگر ناگهان تعداد زیادی heartbeat قطع شد، شاید مشکل از شبکهٔ خودِ Eureka باشد نه از مرگ واقعی سرویس‌ها؛ پس برای احتیاط، هیچ instanceای را حذف نمی‌کنم.» در تولید این یعنی Eureka ممکن است instanceهای مرده را همچنان به کلاینت‌ها تحویل بدهد و تماس‌ها fail شوند. تازه‌کارها self-preservation را در محیط dev خاموش می‌کنند و فراموش می‌کنند در prod چه رفتاری دارد. درسِ سنیور: این رفتار را بشناس، در محیط‌های کوچک dev خاموشش کن (enable-self-preservation: false) اما در prod با آگاهی و تنظیم آستانه‌ها کار کن، و همیشه کلاینت را با circuit breaker + retry مسلح کن تا به داده‌های کهنهٔ registry اتکای کور نکند.

پیاده‌سازی با Consul (HashiCorp)

Consul علاوه بر discovery، یک key/value store (برای config) و health checking فعال دارد. تفاوت مهم با Eureka: Eureka فقط به heartbeat اتکا می‌کند (مدل AP در قضیهٔ CAP — دسترس‌پذیری بالاتر از سازگاری)، اما Consul health checkهای واقعی (HTTP/TCP/script) اجرا می‌کند و بر پایهٔ Raft سازگاری قوی‌تری (CP) دارد. پورت پیش‌فرض 8500.

spring:
  application:
    name: payment-service
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        health-check-path: /actuator/health
        health-check-interval: 10s
        prefer-ip-address: true

Kubernetes: discovery بدون هیچ کتابخانه‌ای

روی Kubernetes، discovery در خودِ پلتفرم پخته شده. وقتی یک Service می‌سازی، Kubernetes یک نام DNS پایدار به آن می‌دهد: payment-service.default.svc.cluster.local. هر تماس به این نام، توسط kube-proxy (با iptables یا IPVS) روی Podهای سالم پشت آن Service پخش می‌شود — یعنی server-side discovery + load balancing، رایگان و زبان‌مستقل.

apiVersion: v1
kind: Service
metadata:
  name: payment-service
spec:
  selector:
    app: payment
  ports:
    - port: 80
      targetPort: 8080

حالا از داخل هر Pod دیگری، فقط کافی است به http://payment-service/api/... بزنی. هیچ Eureka، هیچ کتابخانهٔ discovery.

نسخه‌ها و ابزارها (۲۰۲۶)

این فصل بر پایهٔ Spring Cloud 2025.0.x (Northfields) روی Spring Boot 3.5.x نوشته شده که خط پایدار و پرکاربرد فعلی است. نسخهٔ بعدی، Spring Cloud 2025.1.x (Oakwood)، روی Spring Framework 7 / Spring Boot 4 بنا شده و همهٔ زیرپروژه‌ها به نسخهٔ 5.0.0 رسیده‌اند. اگر روی Boot 4 هستی، artifactها همان نام‌های جدیدی را دارند که پایین‌تر می‌بینی؛ نام‌های قدیمیِ Gateway در Oakwood کاملاً حذف شده‌اند.

مقایسهٔ چهار رویکرد discovery:

ویژگی Eureka Consul Kubernetes DNS (Ribbon — منسوخ)
مدل client-side client-side/server server-side client-side
CAP AP (دسترس‌پذیر) CP (سازگار) CP (etcd/Raft)
Health check heartbeat HTTP/TCP/script probes (liveness/readiness)
Config store ندارد دارد (KV) ConfigMap/Secret
وابستگی زبان Java-centric چندزبانه کاملاً زبان‌مستقل Java
بهترین برای Spring روی VM چند-پلتفرم هرچیزی روی k8s (استفاده نکن)

بخش ۲: Load Balancing سمت‌کلاینت با Spring Cloud LoadBalancer

وقتی registry به تو می‌گوید «Payment سه instance دارد: A، B، C»، حالا باید یکی را انتخاب کنی. این انتخاب همان load balancing است.

صف‌های صندوق سوپرمارکت

سه صندوق باز است. تو به‌عنوان مشتری کدام صف را انتخاب می‌کنی؟ اگر همه بی‌فکر به صندوق اول بروند، آن یکی می‌ترکد و دوتای دیگر بی‌کار می‌مانند. Load balancer آن راهنمای مؤدبی است که می‌گوید «شما صندوق ۲، شما صندوق ۳» تا بار به‌طور یکنواخت پخش شود.

سال‌ها ابزار پیش‌فرض Spring، Netflix Ribbon بود. Ribbon اکنون منسوخ (deprecated) است و جانشین رسمی‌اش Spring Cloud LoadBalancer است — سبک‌تر، reactive-friendly و بخش هستهٔ Spring Cloud. اگر در کدی هنوز Ribbon دیدی، بدان که legacy است.

الگوریتم پیش‌فرض Round Robin است (نوبتی: A، B، C، A، B، C…). یک RandomLoadBalancer هم آماده است. کلید فعال شدن، annotation‌ای به نام @LoadBalanced روی WebClient/RestClient builder است:

@Configuration
public class HttpClientConfig {

  @Bean
  @LoadBalanced
  public WebClient.Builder loadBalancedWebClientBuilder() {
    return WebClient.builder();
  }
}
@Service
public class PaymentClient {

  private final WebClient webClient;

  public PaymentClient(WebClient.Builder builder) {
    // 'payment-service' نام منطقی است، نه host واقعی —
    // LoadBalancer آن را به یک instance سالم ترجمه می‌کند
    this.webClient = builder.baseUrl("http://payment-service").build();
  }

  public Mono<PaymentResult> charge(ChargeRequest req) {
    return webClient.post()
        .uri("/api/charge")
        .bodyValue(req)
        .retrieve()
        .bodyToMono(PaymentResult.class);
  }
}

نکتهٔ مهم: http://payment-service یک URL جعلی به‌نظر می‌رسد اما LoadBalancer آن host را می‌گیرد، از registry (Eureka/Consul/k8s) لیست instanceها را می‌پرسد، یکی را با round-robin انتخاب می‌کند و host را جایگزین می‌کند.

می‌توانی الگوریتم را per-service عوض کنی و کش را تنظیم کنی:

spring:
  cloud:
    loadbalancer:
      cache:
        enabled: true
        ttl: 35s          # چند وقت لیست instanceها کش شود
      health-check:
        interval: 25s
      # ری‌تری داخلی روی instance دیگر هنگام خطا
      retry:
        enabled: true
توزیع بار «هوشمند» یک سراب است

تازه‌کارها فکر می‌کنند round-robin ساده «احمقانه» است و دنبال الگوریتم‌های «least-connections» یا «latency-aware» می‌گردند. واقعیت: در سمت‌کلاینت، هر instanceِ کلاینت فقط بخشی از ترافیک را می‌بیند، پس تصمیم «هوشمند» بدون دید سراسری غالباً بدتر از round-robin می‌شود (پدیدهٔ herd — همه هم‌زمان به instanceای که یک لحظه سریع بود هجوم می‌برند). درس سنیور: round-robin ساده به‌علاوهٔ health check درست و circuit breaker تقریباً همیشه بهتر از یک الگوریتم پیچیدهٔ نیمه‌کور است. پیچیدگی را جایی خرج کن که دید سراسری داری (مثل mesh یا L7 LB).

Service Mesh کجا وارد می‌شود؟

اگر شنیدی «چرا اصلاً discovery و LB را در اپ می‌گذاری؟ بگذارش روی Istio/Linkerd» حق دارند. یک service mesh با sidecar (مثل Envoy) کنار هر Pod، کار discovery، load balancing، retry، mTLS و observability را از کد اپلیکیشن به لایهٔ زیرساخت منتقل می‌کند. مزیت: اپ تو ساده می‌ماند و رفتار شبکه یکنواخت و زبان‌مستقل می‌شود. هزینه: پیچیدگی عملیاتی sidecar‌ها. برای مصاحبه بدان که Spring Cloud LoadBalancer و service mesh دو پاسخ به یک سؤال‌اند و روی k8s بالغ، mesh معمولاً برنده است.


بخش ۳: API Gateway با Spring Cloud Gateway

حالا بالاترین لایه: دروازهٔ لبه (edge). کلاینت بیرونی نباید مستقیم با ۴۰ سرویس حرف بزند. یک نقطهٔ ورودی واحد لازم است.

پذیرش هتل

تصور کن به هتل بزرگی می‌روی. مهمان مستقیم نمی‌رود در اتاق ۴۰۴ را بزند. اول به پذیرش (reception) می‌رود: کارت شناسایی چک می‌شود (auth)، اتاق تخصیص داده می‌شود (routing)، اگر شلوغ باشد صف مدیریت می‌شود (rate limiting) و اگر خدمتی خراب باشد، پذیرش خبر می‌دهد به‌جای اینکه مهمان سرگردان شود (circuit breaking). API Gateway دقیقاً همان پذیرش است: یک نقطهٔ ورود که سیاست‌های مشترک را در «لبه» اعمال می‌کند.

چرا gateway؟ مسئولیت‌های عرضی (cross-cutting)

بدون gateway، هر سرویس باید خودش auth، rate limit، logging، CORS و TLS termination را پیاده کند — تکرار و ناهماهنگی. Gateway این نگرانی‌های عرضی را یک‌جا متمرکز می‌کند:

  • Routing: مسیر دادن درخواست به سرویس درست بر اساس path/host/header.
  • Authentication/Authorization در لبه: اعتبارسنجی JWT قبل از رسیدن به سرویس‌ها.
  • Rate limiting & throttling: محافظت در برابر سیل درخواست.
  • Aggregation ساده و تغییر شکل درخواست/پاسخ.
  • Observability: یک نقطه برای trace و metric ورودی.

نصب: نام‌های جدید artifact (خیلی مهم)

از Spring Cloud 2025.0.0 به بعد، نام ماژول‌ها و starterهای Gateway عوض شده‌اند تا دو سبک gateway (server در برابر proxy-exchange) و دو web-stack (WebFlux در برابر WebMVC) روشن شوند:

نام قدیمی (منسوخ) نام جدید
spring-cloud-starter-gateway spring-cloud-starter-gateway-server-webflux
spring-cloud-starter-gateway-mvc spring-cloud-starter-gateway-server-webmvc
spring-cloud-gateway-mvc spring-cloud-gateway-proxyexchange-webmvc
spring-cloud-gateway-webflux spring-cloud-gateway-proxyexchange-webflux
نام قدیمی dependency در 2025.1 حذف شده

اگر همچنان spring-cloud-starter-gateway را در pom.xml داری، روی 2025.0.x فقط یک warning در لاگ می‌گیری، اما در 2025.1.0 (Oakwood) این artifact کاملاً حذف شده و build می‌شکند. سنیورها این را حین ارتقا به Boot 4 با recipe رسمیِ OpenRewrite (spring-cloud-gateway-deprecated-modules-and-starters) خودکار مهاجرت می‌دهند و prefixهای property را هم با spring-boot-properties-migrator بررسی می‌کنند. اگر فقط دستی نام را عوض کنی و prefix تنظیمات را جا بیندازی، routeها بی‌صدا کار نمی‌کنند.

WebFlux در برابر WebMVC کدام؟ نسخهٔ WebFlux (روی Netty، non-blocking) برای gateway که کارش عمدتاً I/O شبکه‌ای و پراکسی است، انتخاب کلاسیک و بهینه است چون با تعداد thread کم، همزمانی بالا را می‌کشد. نسخهٔ WebMVC (روی servlet/Tomcat، blocking) برای تیم‌هایی است که با مدل reactive راحت نیستند یا کدشان blocking است. برای gateway خالص، WebFlux معمول‌تر است.

سه مفهوم هستهٔ Gateway: Route، Predicate، Filter

  • Route: واحد پایهٔ مسیریابی. شامل یک id، یک uri مقصد، مجموعه‌ای از predicateها و مجموعه‌ای از filterهاست.
  • Predicate: شرطِ «آیا این درخواست به این route می‌خورد؟». مثل «اگر path با /api/orders شروع شد» یا «اگر header فلان بود».
  • Filter: کاری که روی درخواست/پاسخِ منطبق انجام می‌شود — قبل (pre) یا بعد (post) از رفتن به سرویس مقصد.

Request lifecycle inside the gateway / چرخهٔ عمر یک درخواست در gateway:

flowchart LR
  Req[Incoming request] --> Match{Predicate match?}
  Match -->|no| Next[Try next route]
  Match -->|yes| Pre[Pre-filters]
  Pre --> Proxy[Proxy to target service]
  Proxy --> Post[Post-filters]
  Post --> Resp[Response to client]

تعریف route با YAML

spring:
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: orders-route
              uri: lb://order-service      # lb:// یعنی از LoadBalancer + registry استفاده کن
              predicates:
                - Path=/api/orders/**
                - Method=GET,POST
              filters:
                - StripPrefix=1            # /api/orders/5 -> /orders/5
                - name: CircuitBreaker
                  args:
                    name: ordersCb
                    fallbackUri: forward:/fallback/orders
            - id: users-route
              uri: lb://user-service
              predicates:
                - Path=/api/users/**
              filters:
                - AddRequestHeader=X-Gateway, edge

توجه کن uri: lb://order-service — پیشوند lb:// یعنی gateway به‌جای host ثابت، از Spring Cloud LoadBalancer و registry برای resolve کردن نام استفاده می‌کند. این جایی است که هر سه ابزار فصل به هم می‌رسند: Gateway + Discovery + LoadBalancer.

تعریف route با Java DSL (برای منطق پویا)

گاهی به منطقِ برنامه‌نویسیِ بیشتری نیاز داری:

@Configuration
public class GatewayRoutes {

  @Bean
  public RouteLocator routes(RouteLocatorBuilder builder) {
    return builder.routes()
      .route("orders-route", r -> r
          .path("/api/orders/**")
          .filters(f -> f
              .stripPrefix(1)
              .circuitBreaker(c -> c.setName("ordersCb")
                  .setFallbackUri("forward:/fallback/orders"))
              .retry(rc -> rc.setRetries(2)))
          .uri("lb://order-service"))
      .route("users-route", r -> r
          .path("/api/users/**")
          .uri("lb://user-service"))
      .build();
  }
}

Predicateها و Filterهای پرکاربرد:

Predicate معنی
Path=/api/** تطبیق مسیر
Method=GET,POST تطبیق متد HTTP
Header=X-Region, eu-.* تطبیق header با regex
Query=debug وجود پارامتر query
Host=**.example.com تطبیق host
After=<datetime> / Weight=group,8 زمان‌بندی و canary
Filter کاربرد
StripPrefix=1 حذف بخشی از مسیر قبل از forward
AddRequestHeader / AddResponseHeader افزودن header
RewritePath بازنویسی path با regex
RequestRateLimiter محدودسازی نرخ
CircuitBreaker مدار شکن با fallback
Retry تلاش مجدد کنترل‌شده
Gateway، محل منطق دامنه نیست

بزرگ‌ترین anti-pattern در عمل: gateway کم‌کم تبدیل به «مونولیت مخفی» می‌شود. تیم‌ها شروع می‌کنند به گذاشتن منطق کسب‌وکار، تبدیل‌های پیچیده، aggregation چند-سرویسه و حتی دسترسی به دیتابیس داخل filterها. نتیجه: gateway یک نقطهٔ deployment مشترک و شکنندهٔ همه‌ی تیم‌ها می‌شود که هیچ تیمی مالکش نیست. قانون سنیور: gateway فقط سیاست‌های عرضی و مسیریابی را می‌داند، نه دامنه را. هر منطق کسب‌وکاری متعلق به یک سرویس است. اگر aggregation واقعی می‌خواهی، الگوی BFF را به‌کار ببر (پایین‌تر)، نه انبار کردن آن در gateway مشترک.

Rate Limiting: محافظت از پشت‌صحنه

Gateway باید سرویس‌های پشتی را از سیل درخواست محافظت کند. فیلتر RequestRateLimiter این کار را می‌کند و پیاده‌سازی پیش‌فرضش RedisRateLimiter است که از الگوریتم Token Bucket استفاده می‌کند. نیازمند spring-boot-starter-data-redis-reactive است (چون شمارنده باید بین همهٔ instanceهای gateway مشترک باشد — و Redis آن state مشترک است).

spring:
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: orders-route
              uri: lb://order-service
              predicates:
                - Path=/api/orders/**
              filters:
                - name: RequestRateLimiter
                  args:
                    redis-rate-limiter.replenishRate: 10    # ۱۰ توکن/ثانیه پر می‌شود
                    redis-rate-limiter.burstCapacity: 20    # سقف انفجار: ۲۰
                    redis-rate-limiter.requestedTokens: 1   # هزینهٔ هر درخواست
                    key-resolver: "#{@userKeyResolver}"

replenishRate نرخ پایدار مجاز (درخواست بر ثانیه) است؛ burstCapacity بیشترین درخواست لحظه‌ای مجاز (اندازهٔ سطل). KeyResolver تعیین می‌کند نرخ بر چه پایه‌ای شمرده شود — کاربر، IP، API key:

@Bean
KeyResolver userKeyResolver() {
  // نرخ را per-user بشمار؛ اگر کاربر نبود، بیفت روی IP
  return exchange -> {
    String user = exchange.getRequest().getHeaders().getFirst("X-User-Id");
    if (user != null) return Mono.just(user);
    String ip = exchange.getRequest().getRemoteAddress()
                        .getAddress().getHostAddress();
    return Mono.just(ip);
  };
}
Bucket4j به‌عنوان جایگزین

از 2025.0.0، پیاده‌سازی Bucket4jRateLimiter هم در server-webflux پشتیبانی می‌شود. اگر نمی‌خواهی به Redis وابسته شوی یا rate limit محلیِ per-instance کافی است، Bucket4j گزینهٔ سبک‌تری است. اما یادت باشد: rate limit محلی یعنی سقف واقعی = سقف تنظیم‌شده × تعداد instanceهای gateway. برای سقف سراسریِ دقیق، همچنان به یک store مشترک (Redis) نیاز داری.

وقتی Redis می‌میرد، gateway چه می‌کند؟

اگر rate limiter به Redis وابسته است و Redis از دسترس خارج شود، رفتار پیش‌فرض چیست؟ آیا همهٔ درخواست‌ها را رد می‌کنی (fail-closed) یا همه را رها می‌کنی (fail-open)؟ این یک تصمیم معماری است نه یک جزئیات. اگر fail-open باشی، هنگام قطعی Redis محافظت نرخ به‌کل از بین می‌رود و درست وقتی زیرساخت شکننده است، سیل درخواست به سرویس‌ها می‌رسد. سنیورها این رفتار را صریحاً تست و تصمیم‌گیری می‌کنند و monitoring روی سلامت Redis می‌گذارند، نه اینکه در حادثه غافلگیر شوند.

احراز هویت در لبه (Auth at the edge)

الگوی رایج و درست: gateway، JWT را در لبه اعتبارسنجی می‌کند (امضا، انقضا، issuer، audience) و فقط درخواست‌های معتبر را به پشت می‌فرستد. سرویس‌های پشتی می‌توانند اعتماد کمتری بگذارند اما — هشدار — نباید کورکورانه به gateway اعتماد مطلق کنند (اصل zero-trust). gateway معمولاً هویت را در header مثل X-User-Id تزریق می‌کند یا token را relay می‌کند.

Token relay through the gateway / انتقال توکن از میان gateway:

sequenceDiagram
  participant C as Client
  participant G as API Gateway
  participant A as Auth Server (OIDC)
  participant S as Order Service
  C->>G: GET /api/orders (Bearer JWT)
  G->>A: Validate signature via JWKS
  A-->>G: Keys / OK
  G->>G: Check exp, iss, aud, scopes
  G->>S: Forward + relay token / inject X-User-Id
  S-->>G: 200 orders
  G-->>C: 200 orders

با Spring Security روی gateway به‌عنوان OAuth2 Resource Server:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          # gateway کلیدهای عمومی را از JWKS می‌گیرد و امضا را چک می‌کند
          issuer-uri: https://auth.example.com/realms/prod
  cloud:
    gateway:
      server:
        webflux:
          default-filters:
            - TokenRelay=      # token اصلی را به سرویس‌های پشتی relay کن
@Configuration
@EnableWebFluxSecurity
public class EdgeSecurity {

  @Bean
  SecurityWebFilterChain security(ServerHttpSecurity http) {
    return http
        .csrf(ServerHttpSecurity.CsrfSpec::disable)
        .authorizeExchange(ex -> ex
            .pathMatchers("/api/public/**").permitAll()
            .anyExchange().authenticated())
        .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
        .build();
  }
}
Gateway یک نقطهٔ شکست واحد (SPOF) است

همه‌ی ترافیک از gateway می‌گذرد؛ پس اگر gateway بیفتد، کلِ پلتفرم از بیرون قطع می‌شود. تازه‌کارها یک instance از gateway بالا می‌آورند و خوشحال‌اند تا شب حادثه. اصول سنیور: (۱) gateway را stateless نگه دار تا افقی مقیاس بگیرد؛ (۲) حداقل دو-سه instance پشت یک L4/L7 load balancer؛ (۳) روی gateway timeout و circuit breaker بگذار وگرنه یک سرویسِ کند، threadها/connectionهای gateway را می‌بلعد و کل لبه را می‌خواباند (پدیدهٔ cascading failure)؛ (۴) health check و graceful shutdown درست. gateway باید «نازک اما مقاوم» باشد.

timeout‌ها بی‌صدا تو را می‌کشند

پیش‌فرض‌های timeout در HTTP client داخلی gateway اغلب بسیار سخاوتمند یا حتی نامحدودند. یک سرویس پشتیِ کند که هرگز پاسخ نمی‌دهد، اتصالات gateway را نگه می‌دارد تا استخر تمام شود و بعد gateway برای همه می‌افتد. همیشه connect-timeout و response-timeout صریح تنظیم کن (سراسری یا per-route) و آن را با circuit breaker و retry هماهنگ کن. عددها را از SLO سرویس بیرون بکش، نه از حدس.


BFF Pattern — یک Backend برای هر Frontend

یک gateway مشترک برای همهٔ کلاینت‌ها مشکلی دارد: نیازهای اپ موبایل با نیازهای وب و شریک تجاری فرق دارد. موبایل payload کوچک و تعداد round-trip کم می‌خواهد (باتری و شبکهٔ ضعیف)، وب فیلدهای بیشتری می‌خواهد، شریک تجاری قرارداد پایدار و نسخه‌بندی‌شده. اگر یک gateway بخواهد همه را راضی کند، پر از if client == mobile می‌شود.

پیشخدمت مخصوص هر میز

تصور کن یک رستوران که به‌جای یک منوی غول‌پیکر برای همه، برای میز کودکان یک پیشخدمت با منوی ساده و بشقاب‌های کوچک دارد، برای میز مهمانان تجاری یک پیشخدمت با منوی مفصل. هر پیشخدمت با همان آشپزخانه (سرویس‌های پشتی) کار می‌کند اما تجربه را برای مخاطب خودش بهینه می‌کند. BFF یعنی هر نوع frontend یک backend اختصاصیِ نازک دارد که دقیقاً برای آن کلاینت شکل داده شده.

BFF: one tailored backend per client type / یک backend سفارشی برای هر نوع کلاینت:

flowchart TD
  Mobile[Mobile app] --> MBFF[Mobile BFF]
  Web[Web SPA] --> WBFF[Web BFF]
  Partner[Partner API] --> PBFF[Partner BFF]
  MBFF --> Order[Order Service]
  MBFF --> User[User Service]
  WBFF --> Order
  WBFF --> User
  WBFF --> Catalog[Catalog Service]
  PBFF --> Order

هر BFF مسئول aggregation و تغییر شکلِ مخصوصِ آن کلاینت است — همان کاری که نباید در gateway مشترک بگذاری. BFF مالکِ روشنی دارد: تیمِ همان frontend. تفاوت BFF با gateway: gateway سیاست‌های عرضیِ عمومی است؛ BFF منطق تجربهٔ یک کلاینتِ خاص.

BFF می‌تواند به تکثیر بی‌رویه برسد

BFF قدرتمند است اما اگر بی‌مرز به کار رود، برای هر تغییر کوچک UI یک سرویس جدید می‌سازی و ناگهان ۱۲ BFF داری که ۸۰٪ کدشان یکی است. قضاوت سنیور: BFF را بر اساس نوع کلاینت (موبایل/وب/شریک) بشکن، نه بر اساس هر صفحه. کد مشترک را در کتابخانه‌ها بگذار. و BFF را نازک نگه دار — orchestration بله، دامنه نه.


بخش ۴: Config متمرکز با Spring Cloud Config

آخرین ستون: تنظیمات. در مونولیت یک application.yml کافی بود. با ۴۰ سرویس × ۳ محیط (dev/stage/prod)، تنظیمات پراکنده کابوس می‌شود. سؤال کلیدی: چطور یک فلگ را در همهٔ سرویس‌ها عوض کنم بدون rebuild و redeploy؟

تابلوی اعلانات مرکزی کارخانه

تصور کن کارخانه‌ای با ۴۰ خط تولید. اگر بخواهی «سرعت مجاز نوار نقاله» را عوض کنی، نمی‌روی روی هر ۴۰ دستگاه دستی تنظیم کنی. یک تابلوی مرکزی داری؛ عدد را آنجا عوض می‌کنی و همهٔ خطوط آن را می‌خوانند. Spring Cloud Config Server همان تابلوی مرکزی است: تنظیمات را یک‌جا (معمولاً در Git) نگه می‌دارد و سرویس‌ها هنگام بالا آمدن (و در صورت refresh، حین کار) آن را می‌خوانند.

معماری: Config Server + backend

Spring Cloud Config دو تکه است: یک Config Server که تنظیمات را از یک backend (اغلب Git، یا Vault، فایل‌سیستم، S3) سرو می‌کند، و Config Clientها (سرویس‌ها) که موقع راه‌اندازی تنظیماتشان را از سرور می‌گیرند.

سرور:

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
  public static void main(String[] args) {
    SpringApplication.run(ConfigServerApplication.class, args);
  }
}
# Config Server — از یک Git repo سرو می‌کند
server:
  port: 8888
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/acme/config-repo
          default-label: main
          search-paths: '{application}'   # پوشه‌بندی بر اساس نام سرویس

فایل‌های داخل repo با قرارداد نام‌گذاری می‌شوند: payment-service.yml، payment-service-prod.yml، و یک application.yml سراسری برای مقادیر مشترک همهٔ سرویس‌ها.

کلاینت: از bootstrap به spring.config.import

تغییر مهم: bootstrap منسوخ شد

در Spring Cloud قدیم، کلاینت با یک bootstrap.yml و وابستگی spring-cloud-starter-bootstrap به Config Server وصل می‌شد. از Spring Boot 2.4 به بعد، روش رسمی و پیش‌فرض spring.config.import است. اگر در پروژه‌ای هنوز bootstrap.yml می‌بینی، legacy است. روش امروزی:

# کلاینت — application.yml
spring:
  application:
    name: payment-service
  config:
    import: "optional:configserver:http://localhost:8888"
  profiles:
    active: prod

optional: یعنی اگر Config Server در دسترس نبود، اپ به‌جای crash با تنظیمات محلی بالا می‌آید — برای dev خوب است، اما در prod شاید بخواهی optional: را برداری تا اگر config نیامد، اپ عمداً بالا نیاید (fail-fast به‌جای بالا آمدن با تنظیمات غلط).

Profileها: یک کد، چند محیط

Profile مکانیزم Spring برای «همان اپ، تنظیمات متفاوت بر اساس محیط» است. با payment-service-prod.yml مقادیرِ مخصوص prod را روی مقادیر پایهٔ payment-service.yml سوار می‌کنی. ترتیب اولویت (precedence) مهم است: مقادیر خاص‌تر (profile-specific) بر مقادیر عمومی‌تر غلبه می‌کنند، و متغیرهای محیطی/آرگومان خط فرمان بر همه.

Refresh زنده: قلب ماجرا

فایده اصلی config متمرکز این است که بتوانی تنظیم را بدون restart عوض کنی. مکانیزم:

۱. یک bean که می‌خواهی refresh شود را با @RefreshScope علامت بزن. این bean هنگام refresh دوباره ساخته و مقادیر جدید را می‌خواند. ۲. یک POST به /actuator/refresh روی آن instance بزن. این یک RefreshScopeRefreshedEvent منتشر می‌کند و beanهای @RefreshScope بازسازی و @ConfigurationPropertiesها rebind می‌شوند.

@RefreshScope
@Component
public class PricingConfig {
  @Value("${pricing.discount-percent:0}")
  private int discountPercent;   // بعد از refresh، مقدار جدید

  public int discountPercent() { return discountPercent; }
}
# فقط همین instance را refresh می‌کند
curl -X POST http://payment-1:8080/actuator/refresh

اما مشکل: اگر ۲۰ instance از payment-service داری، باید ۲۰ بار curl بزنی؟ اینجا Spring Cloud Bus وارد می‌شود.

Spring Cloud Bus: refresh یک‌جا برای همه

Spring Cloud Bus همهٔ instanceها را از طریق یک message broker (Kafka یا RabbitMQ) به یک «bus» مشترک وصل می‌کند. یک POST به /actuator/busrefresh روی یک instance کافی است: آن instance یک RefreshRemoteApplicationEvent روی broker منتشر می‌کند و همهٔ instanceهای مشترک آن را می‌گیرند و خودشان را refresh می‌کنند.

Broadcast config refresh over the bus / پخش refresh تنظیمات روی bus:

flowchart TD
  Ops[Ops: POST /actuator/busrefresh] --> S1[payment-1]
  S1 -->|publish RefreshRemoteApplicationEvent| Broker[(Kafka / RabbitMQ)]
  Broker --> S1
  Broker --> S2[payment-2]
  Broker --> S3[payment-3]
  S1 --> Cfg[(Config Server -> Git)]
  S2 --> Cfg
  S3 --> Cfg
# کلاینت با bus روی kafka
spring:
  cloud:
    bus:
      enabled: true
    stream:
      kafka:
        binder:
          brokers: kafka:9092
management:
  endpoints:
    web:
      exposure:
        include: busrefresh, refresh, health

معماری بالغ‌تر: یک webhook از Git (روی هر push) به Config Server می‌زند که آن هم busrefresh را ماشه می‌کند — یعنی commit روی repo تنظیمات، خودکار به همهٔ سرویس‌ها propagate می‌شود. GitOps واقعی برای config.

نه‌همه‌چیز با refresh عوض می‌شود

دام رایج: انتظار داری هر تنظیمی با busrefresh زنده عوض شود، اما بعضی چیزها فقط هنگام راه‌اندازی خوانده می‌شوند و refresh آن‌ها را دست نمی‌زند — مثل تنظیمات استخر اتصال دیتابیس، پورت سرور، یا beanهایی که @RefreshScope ندارند. تیم‌ها فلگ را عوض می‌کنند، busrefresh می‌زنند و گیج می‌شوند که چرا اثر نکرد. درس: بدان دقیقاً چه چیزی refreshable است، آن را تست کن، و برای تغییرات غیرقابل‌refresh یک rolling restart برنامه‌ریزی کن. رفتار نیمه‌refresh‌شده (بعضی instanceها نو، بعضی کهنه) هم خودش یک حالت خطرناک است که باید برایش آماده باشی.

مدیریت رازها (Secrets)

گذاشتن رمز دیتابیس به‌صورت plain-text در Git فاجعهٔ امنیتی است. سه رویکرد:

۱. رمزنگاری در Config Server: Config Server می‌تواند مقادیر را با یک کلید رمز کند؛ در repo مقدار به‌صورت {cipher}AQB... ذخیره می‌شود و سرور موقع سرو، آن را رمزگشایی می‌کند. اندپوینت‌های /encrypt و /decrypt برای این کارند.

# در repo، رمز به‌صورت رمزنگاری‌شده
spring:
  datasource:
    password: '{cipher}AQBvJ8x...k9'

۲. HashiCorp Vault به‌عنوان backend: به‌جای Git، رازها را در Vault نگه می‌داری که چرخش (rotation)، اجاره‌های زمان‌دار (leases) و audit دارد. Spring Cloud Config/Vault آن را یکپارچه می‌کند.

۳. Secrets پلتفرم (روی k8s): از Kubernetes Secret (و بهتر، یک راهکار مثل External Secrets Operator یا Sealed Secrets) استفاده می‌کنی و به‌صورت env var یا فایل mount به Pod می‌رسانی. روی k8s این معمولاً تمیزتر از رمزنگاری دستی در Git است.

هرگز راز را در Git ساده commit نکن — حتی خصوصی

یک تصور غلط خطرناک: «repo تنظیمات ما private است، پس رمز plain-text اشکالی ندارد.» تاریخچهٔ Git ابدی است؛ هر رازی که یک‌بار commit شود، حتی اگر بعداً پاکش کنی، در history می‌ماند و باید rotate شود. علاوه بر آن، هر توسعه‌دهنده‌ای که repo را clone می‌کند یک کپی از همهٔ رازهای prod را روی لپ‌تاپش دارد. سنیورها رازها را از config معمولی جدا می‌کنند (Vault یا Secret پلتفرم)، دسترسی را کمینه می‌کنند و روی repo اسکنر راز (مثل gitleaks) در CI می‌گذارند.

config را از دامنه جدا نگه دار

یک قانون سادهٔ سنیور: چیزی که بین محیط‌ها فرق می‌کند → config. چیزی که رفتار دامنه است → کد. اگر مدام مجبوری برای یک تغییر رفتاری، فایل config را دست بزنی، شاید آن رفتار باید در کد و تست باشد نه config. و برعکس، هارد-کد کردن آدرس‌ها و سقف‌ها در کد یعنی هر تغییر یک redeploy می‌خواهد. مرز درست بین این دو، بلوغ عملیاتی یک تیم را نشان می‌دهد.


همه‌چیز کنار هم: یک درخواست کامل

بیایید یک درخواست کامل را از کلاینت تا دیتابیس دنبال کنیم و ببینیم هر سه ابزار کجا نقش می‌بندند.

End-to-end request across all three pillars / یک درخواست سرتاسری از میان هر سه ستون:

sequenceDiagram
  participant C as Client
  participant G as Gateway
  participant R as Registry
  participant O as Order Service
  participant Cfg as Config Server
  C->>G: GET /api/orders (JWT)
  G->>G: Validate JWT, check rate limit
  G->>R: Resolve lb://order-service
  R-->>G: order-2 @ 10.1.4.7:8080
  G->>O: GET /orders (relay token)
  Note over O,Cfg: On startup O had pulled its config
  O->>O: Apply @RefreshScope values
  O-->>G: 200 orders
  G-->>C: 200 orders

ترتیب زمانی: Config Server وقتی سرویس بالا آمده تنظیماتش را داده؛ سرویس خودش را در Registry ثبت کرده؛ حالا درخواست کاربر به Gateway می‌رسد، آنجا auth و rate limit می‌شود، Gateway از Registry آدرس زندهٔ Order را می‌گیرد، load-balance می‌کند، token را relay می‌کند، و پاسخ برمی‌گردد. این رقص هماهنگ همان چیزی است که یک پلتفرم میکروسرویس را کار می‌اندازد.


دید سنیور: anti-patternها و درس‌های تولید

توزیع‌شدگی مخفی و «مونولیت توزیع‌شده»

بدترین سرنوشت: سرویس‌ها را تکه کردی اما آن‌قدر به هم وابسته‌اند که هر تغییری چند deploy هماهنگ می‌خواهد — «مونولیت توزیع‌شده» که بدترینِ هر دو دنیاست (پیچیدگی توزیع + وابستگی مونولیت). gateway، discovery و config این وابستگی را پنهان می‌کنند اما درمان نمی‌کنند. مرزهای سرویس را درست بکش (bounded context)، وگرنه ابزارهای این فصل فقط لوله‌کشیِ یک طراحی بد می‌شوند.

از ساده شروع کن، جای درست پیچیده شو

یک نکتهٔ بلوغ: نیازی نیست روز اول Eureka + Config Server + Bus + Vault + mesh داشته باشی. اگر روی Kubernetes هستی، با Service DNS برای discovery و ConfigMap/Secret برای config شروع کن؛ gateway را وقتی واقعاً چند سرویس بیرونی داری اضافه کن. هر تکه از این پشته یک مؤلفهٔ عملیاتی است که باید monitor، patch و on-call شود. سنیور بودن یعنی دانستن اینکه چه‌چیزی را الان لازم نداری.

observability قبل از مقیاس

وقتی یک درخواست از gateway → دو سرویس → دیتابیس می‌گذرد و کند می‌شود، بدون distributed tracing (مثل OpenTelemetry با trace-id که از gateway شروع و در همهٔ hopها propagate می‌شود) کور می‌مانی. تیم‌هایی که اول ساختند و بعد observability اضافه کردند، ماه‌ها در حوادث تولید سوختند. gateway نقطهٔ طبیعی تولید trace-id ورودی است — از همان‌جا شروع کن.

تفاوت client-side و server-side discovery چیست و کدام را انتخاب می‌کنی؟

در client-side (مثل Eureka + Spring Cloud LoadBalancer) خودِ مصرف‌کننده از registry لیست instanceها را می‌گیرد و مستقیم یکی را صدا می‌زند؛ یک hop کمتر و کنترل الگوریتم، اما هر کلاینت باید کتابخانهٔ discovery داشته باشد (سختی چند-زبانه). در server-side (مثل Kubernetes Service + kube-proxy یا یک L7 LB) کلاینت فقط به یک نام ثابت می‌زند و واسطه مسیریابی می‌کند؛ کلاینت ساده می‌ماند اما یک hop و مؤلفهٔ زیرساخت اضافه دارد. انتخاب من: روی k8s تقریباً همیشه server-side پلتفرم (Service DNS)، چون داشتن discovery موازی در اپ فقط منبع باگ است؛ روی VM/legacy، Eureka منطقی است.

Ribbon چه شد و جانشینش چیست؟

Netflix Ribbon کتابخانهٔ load balancing سمت‌کلاینتِ قدیمی Spring Cloud بود و اکنون منسوخ است. جانشین رسمی Spring Cloud LoadBalancer است — سبک‌تر، با پشتیبانی reactive، و بخش هستهٔ Spring Cloud. الگوریتم پیش‌فرضش Round Robin است، RandomLoadBalancer هم دارد، با @LoadBalanced روی WebClient/RestClient فعال می‌شود و کش instance و retry قابل‌تنظیم دارد. اگر در کدی هنوز Ribbon ببینم، آن را نشانهٔ legacy بودن و کاندیدای مهاجرت می‌دانم.

Route، Predicate و Filter در Spring Cloud Gateway چه فرقی دارند؟

Route واحد مسیریابی است: یک id، یک uri مقصد، مجموعه‌ای predicate و مجموعه‌ای filter. Predicate شرطِ تطبیق است — آیا این درخواست به این route می‌خورد؟ (بر اساس Path، Method، Header، Host، زمان…). Filter کاری است که روی درخواست/پاسخِ منطبق انجام می‌شود، pre (قبل از forward مثل افزودن header یا rate limit) یا post (بعد از پاسخ مثل افزودن header پاسخ). به‌بیان ساده: predicate تصمیم می‌گیرد «آیا»، filter تصمیم می‌گیرد «چه‌کاری».

چطور در Spring Cloud Gateway rate limiting پیاده می‌کنی؟

با فیلتر RequestRateLimiter و پیاده‌سازی پیش‌فرض RedisRateLimiter که الگوریتم Token Bucket دارد و به spring-boot-starter-data-redis-reactive نیاز دارد (Redis برای state مشترک بین instanceهای gateway). سه پارامتر کلیدی: replenishRate (نرخ پایدار توکن/ثانیه)، burstCapacity (سقف انفجار) و requestedTokens (هزینهٔ هر درخواست). یک KeyResolver bean تعیین می‌کند نرخ بر چه پایه‌ای شمرده شود (کاربر/IP/API-key). از 2025.0 گزینهٔ Bucket4jRateLimiter هم هست. نکتهٔ سنیور: تصمیم fail-open/fail-closed هنگام قطعی Redis را صریح بگیر.

چرا نباید منطق کسب‌وکار را در gateway بگذاری؟

چون gateway یک مؤلفهٔ مشترک و مسیر بحرانی همهٔ ترافیک است؛ گذاشتن منطق دامنه در آن یعنی همهٔ تیم‌ها به یک نقطهٔ deployment مشترک قفل می‌شوند، مالکیت گم می‌شود و gateway به «مونولیت مخفی» تبدیل می‌شود. gateway باید فقط نگرانی‌های عرضی (auth، rate limit، routing، observability) را بداند. اگر aggregation یا تغییر شکلِ مخصوص یک کلاینت لازم است، الگوی BFF را به‌کار ببر که مالک روشن (تیم آن frontend) و مرز روشن دارد.

BFF چیست و چه تفاوتی با API Gateway دارد؟

BFF (Backend for Frontend) یعنی هر نوع کلاینت (موبایل، وب، شریک) یک backend نازکِ اختصاصی دارد که aggregation و تغییر شکل را برای همان کلاینت بهینه می‌کند. تفاوت با gateway: gateway سیاست‌های عرضیِ عمومی را برای همهٔ ترافیک اعمال می‌کند و دامنه‌آگاه نیست؛ BFF منطق تجربهٔ یک کلاینت خاص را می‌داند و مالکش تیم همان frontend است. اغلب هم استفاده می‌شوند: gateway در لبه برای auth/rate-limit، و پشتش چند BFF. مراقب تکثیر بی‌رویهٔ BFF (یکی برای هر صفحه) باش؛ بر اساس نوع کلاینت بشکن.

تفاوت Eureka و Consul در چیست؟

هر دو registry هستند اما فلسفهٔ متفاوت. Eureka روی heartbeat و در دستهٔ AP قضیهٔ CAP است (دسترس‌پذیری را بر سازگاری ترجیح می‌دهد؛ در پارتیشن شبکه ترجیح می‌دهد داده‌های شاید-کهنه بدهد تا هیچ)، و مکانیزم self-preservation دارد. Consul health checkهای واقعی (HTTP/TCP/script) اجرا می‌کند، بر پایهٔ Raft سازگاری قوی‌تر (CP) دارد، و علاوه بر discovery یک KV store برای config و پشتیبانی چند-زبانه ارائه می‌کند. برای اکوسیستم صرفاً Spring، Eureka ساده‌تر است؛ برای محیط چند-پلتفرم با نیاز به config و health دقیق، Consul.

چطور تنظیمات را بدون restart در همهٔ سرویس‌ها refresh می‌کنی؟

تک‌instance: bean را @RefreshScope بزن و POST /actuator/refresh کن که RefreshScopeRefreshedEvent منتشر و beanها بازسازی می‌شوند. برای همهٔ instanceها همزمان: Spring Cloud Bus روی Kafka/RabbitMQ. یک POST /actuator/busrefresh روی یک instance، یک RefreshRemoteApplicationEvent روی broker منتشر می‌کند و همه refresh می‌شوند. بالغ‌ترین حالت: webhook از Git روی هر push، busrefresh را ماشه می‌کند. اما بدان همه‌چیز refreshable نیست — پورت، استخر اتصال و beanهای بدون @RefreshScope فقط با restart عوض می‌شوند.

چرا Gateway یک SPOF است و چطور کاهشش می‌دهی؟

چون همهٔ ترافیک ورودی از آن می‌گذرد؛ افتادنش یعنی قطع کل پلتفرم از بیرون. کاهش ریسک: gateway را stateless نگه دار تا افقی مقیاس بگیرد؛ چند instance پشت یک L4/L7 LB اجرا کن؛ timeout و circuit breaker صریح بگذار تا یک سرویسِ کند، استخر اتصال gateway را نبلعد و cascading failure نسازد؛ health check و graceful shutdown درست؛ و observability با trace-id از خودِ gateway. فلسفه: gateway باید نازک اما بسیار مقاوم باشد.

رازها (secrets) را در پلتفرم config چطور مدیریت می‌کنی؟

هرگز plain-text در Git — حتی repo خصوصی، چون history ابدی است و هر clone یک کپی از رازهاست. سه رویکرد: (۱) رمزنگاری در Config Server با {cipher} و اندپوینت‌های encrypt/decrypt؛ (۲) استفاده از HashiCorp Vault به‌عنوان backend که rotation، lease و audit دارد؛ (۳) روی Kubernetes از Secret پلتفرم (بهتر با External Secrets/Sealed Secrets). رازها را از config معمولی جدا کن، دسترسی را کمینه کن، و اسکنر راز در CI بگذار. اگر رازی لو رفت، rotate واجب است نه فقط پاک کردن commit.

پیشوند lb:// در uri یک route چه می‌کند؟

یعنی gateway به‌جای اتصال به یک host ثابت، نام سرویس بعد از lb:// را از طریق Spring Cloud LoadBalancer و registry (Eureka/Consul/k8s) resolve می‌کند: لیست instanceهای سالم را می‌گیرد، یکی را با round-robin انتخاب می‌کند و درخواست را به آن می‌فرستد. این دقیقاً نقطهٔ تلاقی هر سه ستون فصل است — routing (Gateway) روی discovery (Registry) با load balancing سمت‌کلاینت. اگر به‌جایش http://host:port بنویسی، gateway مستقیم و بدون discovery وصل می‌شود.

روی Kubernetes آیا هنوز به Eureka و Spring Cloud Config نیاز داری؟

غالباً نه به‌طور کامل. Kubernetes خودش discovery (Service + DNS + kube-proxy) و config (ConfigMap/Secret) را به‌صورت بومی و زبان‌مستقل می‌دهد، پس افزودن Eureka معمولاً افزونگیِ زیان‌بار است. اما Spring Cloud Config هنوز ارزش دارد اگر بخواهی refresh زندهٔ بدون rolling restart، تاریخچهٔ Git، رمزنگاری متمرکز یا یک منبع واحد بین k8s و غیر-k8s داشته باشی — چون ConfigMap عوض شدنش به‌طور پیش‌فرض بدون restart در اپ منعکس نمی‌شود (مگر با ابزار کمکی). پاسخ بالغ: «به پلتفرم تکیه می‌کنم مگر جایی که Config Server قابلیتی می‌دهد که k8s به‌سادگی نمی‌دهد.»

چطور جلوی cascading failure از طریق gateway را می‌گیری؟

با ترکیبی از الگوها در لبه: timeout صریح (connect و response) تا یک سرویس کند اتصالات gateway را حبس نکند؛ circuit breaker (مثل Resilience4j) که وقتی یک سرویس خراب است سریع fail و به fallback می‌رود به‌جای انتظار؛ retry محدود و ایمن (فقط روی idempotentها)؛ bulkhead برای جدا کردن استخر منابع هر مقصد تا خرابی یکی، بقیه را نخوابد؛ و rate limiting برای جلوگیری از overload. کلید ذهنی: هر منبعِ محدود (thread، connection) باید سقف و مهلت داشته باشد؛ منبعِ بی‌مهلت، عاملِ فروپاشیِ زنجیره‌ای است.

جمع‌بندی فصل
  • سه ستون عملیاتی میکروسرویس: Gateway (دروازهٔ لبه)، Service Discovery (دفترچه‌تلفن زنده)، Config (اتاق کنترل تنظیمات).
  • Discovery: client-side (Eureka + Spring Cloud LoadBalancer) در برابر server-side (Kubernetes Service/DNS). روی k8s معمولاً به پلتفرم تکیه کن. Eureka self-preservation و AP، Consul health واقعی و CP.
  • Load Balancing: Ribbon منسوخ؛ جانشین Spring Cloud LoadBalancer با round-robin و @LoadBalanced. round-robin ساده + health + circuit breaker معمولاً بهتر از الگوریتم نیمه‌کور است.
  • Gateway: Spring Cloud Gateway با Route/Predicate/Filter؛ نام‌های جدید artifact از 2025.0 (spring-cloud-starter-gateway-server-webflux)؛ rate limiting با RedisRateLimiter (Token Bucket)؛ auth در لبه با JWT و TokenRelay؛ lb:// برای اتصال به registry. gateway را نازک، stateless، با timeout و circuit breaker نگه دار و هرگز دامنه در آن نگذار.
  • BFF: یک backend سفارشی برای هر نوع کلاینت؛ aggregation و تغییر شکل مخصوص کلاینت، نه در gateway مشترک.
  • Config: Spring Cloud Config با backend گیت؛ spring.config.import به‌جای bootstrap؛ profileها برای محیط‌ها؛ refresh با @RefreshScope + /actuator/refresh و پخش سراسری با Spring Cloud Bus (/actuator/busrefresh)؛ رازها با Vault/رمزنگاری، هرگز plain-text در Git.
  • قضاوت سنیور: از ساده شروع کن، observability را زودتر بگذار، مرزهای سرویس را درست بکش تا «مونولیت توزیع‌شده» نسازی، و هر منبع محدود را با timeout و circuit breaker محافظت کن.

The moment you migrate from a monolith to microservices, three new and unforgiving questions land on the table — questions that simply did not exist in the monolithic world:

  1. Should the client talk to dozens of different services directly? How would it even know each one's address?
  2. How does Service A find Service B's address when, at any instant, three instances of B may be spinning up or dying?
  3. How is configuration (DB connection strings, feature flags, rate caps) managed across 40 services without redeploying 40 times?

This chapter answers exactly these three operational pains: the API Gateway (the entry doorway), Service Discovery (the live phone book of services), and Centralized Config (the control room for settings). Together these are the nervous system of a microservice architecture. If domain logic is the heart of the system, these are the veins and nerves keeping that heart alive.

Roadmap for this chapter
  • The core problem: why the monolith didn't have these pains and microservices do.
  • Service Discovery: client-side vs server-side discovery, Eureka, Consul, Kubernetes DNS.
  • Load Balancing: client-side balancing with Spring Cloud LoadBalancer (the successor to Ribbon).
  • API Gateway: Spring Cloud Gateway — routes, predicates, filters, rate limiting, edge authentication.
  • BFF Pattern: one gateway per client type.
  • Centralized Config: Spring Cloud Config, profiles, live refresh, secrets management.
  • Senior lens: production traps, anti-patterns, and how to talk about all of this in an interview.

Why didn't the monolith have these pains?

In a monolith, when the "Order" module wanted to call the "User" module, it was just a plain method call inside the same JVM: userService.findById(id). There was no address, because everything lived in one process. Configuration was a single application.yml. And the client knew exactly one address: the monolith itself.

Microservices break this convenience — and hand you scalability and team autonomy in return. But that simple method call is now a network call over TCP, and the network is unreliable, slow, and variable. Addresses have become dynamic because in Kubernetes a Pod dies at any moment and comes back with a new IP. These three tools exist precisely to fill that gap.

An airport instead of your kitchen

A monolith is like your own kitchen: you want the salt, you reach out and grab it. Microservices are like an international airport. A passenger (the client) can't just wander up to every gate every time. The API Gateway is the arrivals hall and the check-in desk: it checks your ticket (auth), manages the queue (rate limiting), and directs you to the right gate (routing). Service Discovery is the live departures board overhead that constantly updates which flight leaves from which gate. The Config Server is the airport control room that keeps the rules in one place and broadcasts them to everyone.

Here's the big picture. Bird's-eye topology of a microservice platform / نمای کلی توپولوژی یک پلتفرم میکروسرویس:

flowchart TD
  Client[Mobile / Web / Partner] --> GW[API Gateway]
  GW -->|discovery| Reg[(Service Registry)]
  GW --> Order[Order Service]
  GW --> User[User Service]
  GW --> Pay[Payment Service]
  Order -. register .-> Reg
  User -. register .-> Reg
  Pay -. register .-> Reg
  Cfg[Config Server] -. config .-> Order
  Cfg -. config .-> User
  Cfg -. config .-> Pay
  Cfg --> Git[(Git repo)]
  Order --> OrderDB[(Order DB)]
  User --> UserDB[(User DB)]

The three key actors on this map — the Gateway, the Registry, and the Config Server — are the subject of this chapter. We'll take them one at a time, from the lowest layer (finding services) up to the highest (the edge doorway).


Part 1: Service Discovery — the live phone book of services

Suppose the Order service wants to call Payment. What's its address? If you hardcode http://192.168.4.11:8080, that's a disaster waiting to happen: tomorrow that Pod dies, the IP changes, three fresh instances come up. Hardcoding an address in a dynamic world makes no sense.

A phone book vs memorized numbers

Hardcoding an IP is like memorizing a friend's phone number. The day they change it, you're stuck. Service Discovery means that instead of memorizing the number, you call a "central directory" and ask: "What is Payment's current number?" That directory is always current because every service registers itself when it starts and is removed when it dies.

Concept: Registry, Registration, and Resolution

Build the three terms from scratch:

  • Service Registry: a live database holding "service name → list of current healthy addresses". Examples: Eureka, Consul, etcd (under Kubernetes).
  • Registration: the act by which an instance, on startup, registers itself with a name and address, then periodically sends a "heartbeat" to say "I'm still alive".
  • Resolution (Discovery): the act by which a consumer translates a logical name (payment-service) into a real address.

Client-side vs server-side discovery

This distinction is a favorite interview question. There are two fundamental models:

Client-side discovery: the consumer itself pulls the list of addresses from the registry, picks one (load balancing), and calls it directly. Netflix Eureka + Spring Cloud LoadBalancer is exactly this model. Upside: one fewer network hop and precise control over the selection algorithm. Downside: every client must carry discovery and load-balancing logic (a library dependency, and awkward for polyglot stacks).

Server-side discovery: the client only hits one fixed address (a load balancer or gateway), and that intermediary uses the registry to route to the right instance. A Kubernetes Service is exactly this: you hit one stable DNS name and kube-proxy spreads the load across the Pods behind the scenes. Upside: the client needs no logic. Downside: one extra hop and one extra piece of infrastructure.

Client-side vs server-side discovery / مقایسهٔ کشف سمت‌کلاینت و سمت‌سرور:

flowchart LR
  subgraph ClientSide[Client-side discovery]
    C1[Consumer] -->|1 ask| R1[(Registry)]
    C1 -->|2 pick & call| P1[Provider instance]
  end
  subgraph ServerSide[Server-side discovery]
    C2[Consumer] -->|call fixed name| LB[Load Balancer / Service]
    LB -->|route| P2[Provider instance]
    LB -.-> R2[(Registry)]
  end
Senior judgment: which model?

If you're on Kubernetes, you usually don't need Eureka. Kubernetes already has a registry (etcd) and built-in server-side discovery (Service + DNS + kube-proxy). Adding Eureka on top of Kubernetes means two parallel discovery systems that don't know about each other — a source of strange bugs. Seniors keep Eureka mostly for non-Kubernetes environments (VMs, Cloud Foundry) or legacy Spring Cloud systems. The golden interview line: "On k8s, I delegate service discovery to the platform, not to an application library."

Implementation with Eureka (Netflix)

Eureka comes from the Spring Cloud Netflix project. You run a Eureka Server that plays the registry role, and clients register themselves with it. The default port is 8761.

Server:

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
  public static void main(String[] args) {
    SpringApplication.run(DiscoveryServerApplication.class, args);
  }
}
# application.yml — Eureka Server
server:
  port: 8761
eureka:
  client:
    register-with-eureka: false   # the server doesn't register itself
    fetch-registry: false
  server:
    enable-self-preservation: true   # defaults to true — important! explained below

A consumer service registers automatically just by having the dependency and the server URL:

# application.yml — an ordinary service
spring:
  application:
    name: payment-service     # this name is the discovery key
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
    lease-renewal-interval-in-seconds: 30    # heartbeat every 30s (default)
    lease-expiration-duration-in-seconds: 90 # evict after 90s of no heartbeat

Now Order can call Payment by its logical name (not its IP), provided it has a load-balanced RestClient/WebClient, which we build in the next section.

Classic production trap: Self-Preservation Mode

Eureka has a defensive mechanism called self-preservation, on by default. Its logic: "If a large number of heartbeats suddenly stop, maybe the problem is Eureka's own network rather than services actually dying; so to be safe, I won't evict any instance." In production this means Eureka may keep handing dead instances to clients, and calls fail. Beginners disable self-preservation in dev and forget how it behaves in prod. The senior lesson: understand this behavior, disable it in small dev environments (enable-self-preservation: false), but in prod tune the thresholds deliberately — and always arm the client with a circuit breaker + retry so it never blindly trusts stale registry data.

Implementation with Consul (HashiCorp)

Beyond discovery, Consul offers a key/value store (for config) and active health checking. A key difference from Eureka: Eureka relies only on heartbeats (AP in CAP terms — availability over consistency), whereas Consul runs real health checks (HTTP/TCP/script) and, being Raft-based, leans toward stronger consistency (CP). Default port 8500.

spring:
  application:
    name: payment-service
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        health-check-path: /actuator/health
        health-check-interval: 10s
        prefer-ip-address: true

Kubernetes: discovery with no library at all

On Kubernetes, discovery is baked into the platform. When you create a Service, Kubernetes gives it a stable DNS name: payment-service.default.svc.cluster.local. Any call to that name is spread by kube-proxy (via iptables or IPVS) across the healthy Pods behind the Service — that is, server-side discovery + load balancing, free and language-agnostic.

apiVersion: v1
kind: Service
metadata:
  name: payment-service
spec:
  selector:
    app: payment
  ports:
    - port: 80
      targetPort: 8080

Now from inside any other Pod you simply hit http://payment-service/api/.... No Eureka, no discovery library.

Versions and tooling (2026)

This chapter is written against Spring Cloud 2025.0.x (Northfields) on Spring Boot 3.5.x, the current stable and widely-used line. The next release, Spring Cloud 2025.1.x (Oakwood), is built on Spring Framework 7 / Spring Boot 4 and bumps every subproject to 5.0.0. If you're on Boot 4, the artifacts carry the new names you'll see below; the old Gateway names were fully removed in Oakwood.

Comparing four discovery approaches:

Trait Eureka Consul Kubernetes DNS (Ribbon — deprecated)
Model client-side client-side/server server-side client-side
CAP AP (available) CP (consistent) CP (etcd/Raft)
Health check heartbeat HTTP/TCP/script probes (liveness/readiness)
Config store no yes (KV) ConfigMap/Secret
Language ties Java-centric polyglot fully language-agnostic Java
Best for Spring on VMs multi-platform anything on k8s (don't use)

Part 2: Client-side load balancing with Spring Cloud LoadBalancer

When the registry tells you "Payment has three instances: A, B, C", you now have to pick one. That choice is load balancing.

Supermarket checkout lanes

Three checkout lanes are open. Which one do you, as a shopper, pick? If everyone mindlessly heads to the first lane, it collapses under the load while the other two sit idle. A load balancer is the polite usher saying "you go to lane 2, you go to lane 3" so the load spreads evenly.

For years, Spring's default was Netflix Ribbon. Ribbon is now deprecated, and its official successor is Spring Cloud LoadBalancer — lighter, reactive-friendly, and a core part of Spring Cloud. If you still see Ribbon in some code, know that it's legacy.

The default algorithm is Round Robin (in turn: A, B, C, A, B, C…). A RandomLoadBalancer is also available. The switch that turns it on is an annotation called @LoadBalanced on your WebClient/RestClient builder:

@Configuration
public class HttpClientConfig {

  @Bean
  @LoadBalanced
  public WebClient.Builder loadBalancedWebClientBuilder() {
    return WebClient.builder();
  }
}
@Service
public class PaymentClient {

  private final WebClient webClient;

  public PaymentClient(WebClient.Builder builder) {
    // 'payment-service' is a logical name, not a real host —
    // LoadBalancer resolves it to a healthy instance
    this.webClient = builder.baseUrl("http://payment-service").build();
  }

  public Mono<PaymentResult> charge(ChargeRequest req) {
    return webClient.post()
        .uri("/api/charge")
        .bodyValue(req)
        .retrieve()
        .bodyToMono(PaymentResult.class);
  }
}

The key point: http://payment-service looks like a fake URL, but LoadBalancer takes that host, asks the registry (Eureka/Consul/k8s) for the list of instances, picks one round-robin, and substitutes the host.

You can change the algorithm per-service and tune the cache:

spring:
  cloud:
    loadbalancer:
      cache:
        enabled: true
        ttl: 35s          # how long the instance list is cached
      health-check:
        interval: 25s
      # internal retry on a different instance on failure
      retry:
        enabled: true
"Smart" load distribution is often a mirage

Beginners think plain round-robin is "dumb" and go hunting for "least-connections" or "latency-aware" algorithms. The reality: on the client side, each client instance only sees part of the traffic, so a "smart" decision without a global view often does worse than round-robin (the herd effect — everyone simultaneously stampedes toward whichever instance was momentarily fast). The senior lesson: plain round-robin plus a proper health check and a circuit breaker almost always beats a complicated, half-blind algorithm. Spend complexity where you have a global view (a mesh or an L7 LB).

Where does a Service Mesh fit?

If someone asks "why put discovery and LB in the app at all? Push it to Istio/Linkerd," they have a point. A service mesh with a sidecar (like Envoy) next to each Pod moves discovery, load balancing, retries, mTLS, and observability out of application code and into the infrastructure layer. Upside: your app stays simple and network behavior becomes uniform and language-agnostic. Cost: the operational complexity of sidecars. For interviews, know that Spring Cloud LoadBalancer and a service mesh are two answers to the same question, and on a mature k8s platform the mesh usually wins.


Part 3: The API Gateway with Spring Cloud Gateway

Now the top layer: the edge. External clients shouldn't talk to 40 services directly. You need a single entry point.

The hotel front desk

Imagine walking into a large hotel. A guest doesn't march straight up to knock on room 404. They go to the front desk first: ID is checked (auth), a room is assigned (routing), if it's busy a queue is managed (rate limiting), and if some service is broken, the desk informs you instead of leaving you wandering (circuit breaking). The API Gateway is exactly that front desk: a single entry point that applies shared policies at the "edge".

Why a gateway? Cross-cutting concerns

Without a gateway, every service has to implement its own auth, rate limiting, logging, CORS, and TLS termination — duplication and inconsistency. The gateway centralizes these cross-cutting concerns:

  • Routing: dispatch a request to the right service based on path/host/header.
  • Authentication/Authorization at the edge: validate JWTs before they reach services.
  • Rate limiting & throttling: protect against a flood of requests.
  • Simple aggregation and request/response transformation.
  • Observability: one place to start traces and collect ingress metrics.

Installation: the new artifact names (very important)

As of Spring Cloud 2025.0.0, the Gateway module and starter names changed to clarify the two gateway styles (server vs proxy-exchange) and the two web stacks (WebFlux vs WebMVC):

Old name (deprecated) New name
spring-cloud-starter-gateway spring-cloud-starter-gateway-server-webflux
spring-cloud-starter-gateway-mvc spring-cloud-starter-gateway-server-webmvc
spring-cloud-gateway-mvc spring-cloud-gateway-proxyexchange-webmvc
spring-cloud-gateway-webflux spring-cloud-gateway-proxyexchange-webflux
The old dependency name was removed in 2025.1

If you still have spring-cloud-starter-gateway in your pom.xml, on 2025.0.x you only get a warning in the logs — but in 2025.1.0 (Oakwood) that artifact is fully removed and the build breaks. Seniors migrate this automatically during the Boot 4 upgrade with the official OpenRewrite recipe (spring-cloud-gateway-deprecated-modules-and-starters) and also check property prefixes with spring-boot-properties-migrator. If you only rename the dependency by hand and forget the config prefix, routes silently stop working.

WebFlux or WebMVC? The WebFlux flavor (on Netty, non-blocking) is the classic, optimal choice for a gateway whose job is mostly network I/O and proxying, since it sustains high concurrency with few threads. The WebMVC flavor (on a servlet/Tomcat, blocking) suits teams that aren't comfortable with the reactive model or whose code is blocking. For a pure gateway, WebFlux is more common.

The three core Gateway concepts: Route, Predicate, Filter

  • Route: the basic unit of routing. It has an id, a target uri, a set of predicates, and a set of filters.
  • Predicate: the condition "does this request match this route?" Like "if the path starts with /api/orders" or "if a header equals something."
  • Filter: the work performed on a matched request/response — before (pre) or after (post) it goes to the target service.

Request lifecycle inside the gateway / چرخهٔ عمر یک درخواست در gateway:

flowchart LR
  Req[Incoming request] --> Match{Predicate match?}
  Match -->|no| Next[Try next route]
  Match -->|yes| Pre[Pre-filters]
  Pre --> Proxy[Proxy to target service]
  Proxy --> Post[Post-filters]
  Post --> Resp[Response to client]

Defining a route with YAML

spring:
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: orders-route
              uri: lb://order-service      # lb:// means use LoadBalancer + registry
              predicates:
                - Path=/api/orders/**
                - Method=GET,POST
              filters:
                - StripPrefix=1            # /api/orders/5 -> /orders/5
                - name: CircuitBreaker
                  args:
                    name: ordersCb
                    fallbackUri: forward:/fallback/orders
            - id: users-route
              uri: lb://user-service
              predicates:
                - Path=/api/users/**
              filters:
                - AddRequestHeader=X-Gateway, edge

Notice uri: lb://order-service — the lb:// prefix means that instead of a fixed host, the gateway uses Spring Cloud LoadBalancer and the registry to resolve the name. This is where all three tools of the chapter meet: Gateway + Discovery + LoadBalancer.

Defining routes with the Java DSL (for dynamic logic)

Sometimes you need more programmatic logic:

@Configuration
public class GatewayRoutes {

  @Bean
  public RouteLocator routes(RouteLocatorBuilder builder) {
    return builder.routes()
      .route("orders-route", r -> r
          .path("/api/orders/**")
          .filters(f -> f
              .stripPrefix(1)
              .circuitBreaker(c -> c.setName("ordersCb")
                  .setFallbackUri("forward:/fallback/orders"))
              .retry(rc -> rc.setRetries(2)))
          .uri("lb://order-service"))
      .route("users-route", r -> r
          .path("/api/users/**")
          .uri("lb://user-service"))
      .build();
  }
}

Common predicates and filters:

Predicate Meaning
Path=/api/** match path
Method=GET,POST match HTTP method
Header=X-Region, eu-.* match header by regex
Query=debug presence of a query param
Host=**.example.com match host
After=<datetime> / Weight=group,8 scheduling and canary
Filter Use
StripPrefix=1 drop path segments before forwarding
AddRequestHeader / AddResponseHeader add a header
RewritePath rewrite the path with regex
RequestRateLimiter throttle the rate
CircuitBreaker circuit breaker with fallback
Retry controlled retry
The gateway is not where domain logic lives

The biggest anti-pattern in practice: the gateway gradually becomes a "hidden monolith". Teams start putting business logic, complex transformations, multi-service aggregation, and even database access inside filters. The result: the gateway becomes a shared, fragile deployment point for all teams that no single team owns. The senior rule: the gateway knows cross-cutting policy and routing, not the domain. Any business logic belongs to a service. If you genuinely need aggregation, use the BFF pattern (below), not a stockpile in a shared gateway.

Rate Limiting: protecting the backend

The gateway must protect backend services from a flood of requests. The RequestRateLimiter filter does this, and its default implementation is RedisRateLimiter, which uses the Token Bucket algorithm. It requires spring-boot-starter-data-redis-reactive (because the counter must be shared across all gateway instances — and Redis is that shared state).

spring:
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: orders-route
              uri: lb://order-service
              predicates:
                - Path=/api/orders/**
              filters:
                - name: RequestRateLimiter
                  args:
                    redis-rate-limiter.replenishRate: 10    # 10 tokens/sec refilled
                    redis-rate-limiter.burstCapacity: 20    # burst ceiling: 20
                    redis-rate-limiter.requestedTokens: 1   # cost per request
                    key-resolver: "#{@userKeyResolver}"

replenishRate is the steady allowed rate (requests per second); burstCapacity is the maximum momentary requests allowed (the bucket size). The KeyResolver decides what the rate is counted against — user, IP, API key:

@Bean
KeyResolver userKeyResolver() {
  // count the rate per-user; fall back to IP if no user
  return exchange -> {
    String user = exchange.getRequest().getHeaders().getFirst("X-User-Id");
    if (user != null) return Mono.just(user);
    String ip = exchange.getRequest().getRemoteAddress()
                        .getAddress().getHostAddress();
    return Mono.just(ip);
  };
}
Bucket4j as an alternative

As of 2025.0.0, a Bucket4jRateLimiter implementation is also supported in server-webflux. If you don't want a Redis dependency, or a local per-instance rate limit is enough, Bucket4j is a lighter option. But remember: a local rate limit means the real ceiling = configured ceiling × number of gateway instances. For an accurate global ceiling you still need a shared store (Redis).

When Redis dies, what does the gateway do?

If the rate limiter depends on Redis and Redis becomes unavailable, what's the default behavior? Do you reject all requests (fail-closed) or let them all through (fail-open)? This is an architectural decision, not a detail. If you fail-open, rate protection vanishes entirely during a Redis outage — and precisely when your infrastructure is fragile, a flood reaches your services. Seniors explicitly test and decide this behavior, and put monitoring on Redis health, rather than being surprised in an incident.

Authentication at the edge

The common, correct pattern: the gateway validates the JWT at the edge (signature, expiry, issuer, audience) and only forwards valid requests to the back. Backend services can lower their guard somewhat — but, a warning, they must not blindly place absolute trust in the gateway (the zero-trust principle). The gateway typically injects identity into a header like X-User-Id or relays the token.

Token relay through the gateway / انتقال توکن از میان gateway:

sequenceDiagram
  participant C as Client
  participant G as API Gateway
  participant A as Auth Server (OIDC)
  participant S as Order Service
  C->>G: GET /api/orders (Bearer JWT)
  G->>A: Validate signature via JWKS
  A-->>G: Keys / OK
  G->>G: Check exp, iss, aud, scopes
  G->>S: Forward + relay token / inject X-User-Id
  S-->>G: 200 orders
  G-->>C: 200 orders

With Spring Security on the gateway as an OAuth2 Resource Server:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          # gateway fetches public keys from JWKS and verifies the signature
          issuer-uri: https://auth.example.com/realms/prod
  cloud:
    gateway:
      server:
        webflux:
          default-filters:
            - TokenRelay=      # relay the original token to backend services
@Configuration
@EnableWebFluxSecurity
public class EdgeSecurity {

  @Bean
  SecurityWebFilterChain security(ServerHttpSecurity http) {
    return http
        .csrf(ServerHttpSecurity.CsrfSpec::disable)
        .authorizeExchange(ex -> ex
            .pathMatchers("/api/public/**").permitAll()
            .anyExchange().authenticated())
        .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
        .build();
  }
}
The gateway is a single point of failure (SPOF)

All traffic passes through the gateway; so if the gateway falls, the entire platform is cut off from the outside. Beginners run a single gateway instance and are happy — until the night of the incident. Senior principles: (1) keep the gateway stateless so it scales horizontally; (2) run at least two or three instances behind an L4/L7 load balancer; (3) put timeouts and circuit breakers on the gateway, or one slow service will swallow the gateway's threads/connections and take the whole edge down (the cascading failure phenomenon); (4) proper health checks and graceful shutdown. The gateway must be "thin but resilient".

Timeouts kill you silently

The default timeouts in the gateway's internal HTTP client are often very generous or even unbounded. A slow backend that never responds holds the gateway's connections until the pool is exhausted, and then the gateway falls for everyone. Always set explicit connect-timeout and response-timeout (globally or per-route) and align them with your circuit breaker and retry settings. Derive the numbers from the service's SLO, not from a guess.


The BFF Pattern — a Backend for each Frontend

A single shared gateway for all clients has a problem: a mobile app's needs differ from the web's and a business partner's. Mobile wants small payloads and few round-trips (battery and weak networks), the web wants more fields, the partner wants a stable, versioned contract. If one gateway tries to please everyone, it fills up with if client == mobile.

A dedicated waiter per table

Imagine a restaurant that, instead of one giant menu for everyone, gives the kids' table a waiter with a simple menu and small plates, and the business guests a waiter with a detailed menu. Each waiter works with the same kitchen (the backend services) but optimizes the experience for their audience. BFF means each frontend type has a dedicated thin backend shaped precisely for that client.

BFF: one tailored backend per client type / یک backend سفارشی برای هر نوع کلاینت:

flowchart TD
  Mobile[Mobile app] --> MBFF[Mobile BFF]
  Web[Web SPA] --> WBFF[Web BFF]
  Partner[Partner API] --> PBFF[Partner BFF]
  MBFF --> Order[Order Service]
  MBFF --> User[User Service]
  WBFF --> Order
  WBFF --> User
  WBFF --> Catalog[Catalog Service]
  PBFF --> Order

Each BFF owns the aggregation and client-specific shaping — exactly the work you must not put in the shared gateway. A BFF has a clear owner: the team that owns that frontend. The difference between a BFF and a gateway: a gateway is generic cross-cutting policy; a BFF is the experience logic of one specific client.

BFF can spiral into proliferation

The BFF is powerful, but if used without boundaries you spin up a new service for every small UI change and suddenly have 12 BFFs that are 80% the same code. Senior judgment: split BFFs by client type (mobile/web/partner), not by every page. Put shared code in libraries. And keep the BFF thin — orchestration yes, domain no.


Part 4: Centralized Config with Spring Cloud Config

The final pillar: configuration. In a monolith, one application.yml was enough. With 40 services × 3 environments (dev/stage/prod), scattered config becomes a nightmare. The key question: how do I change one flag across all services without a rebuild and redeploy?

The factory's central control board

Picture a factory with 40 production lines. If you want to change the "allowed conveyor speed", you don't walk up to each of the 40 machines and set it by hand. You have a central board; you change the number there and all the lines read it. Spring Cloud Config Server is that central board: it keeps configuration in one place (usually in Git) and services read it on startup (and, with refresh, while running).

Architecture: Config Server + backend

Spring Cloud Config is two pieces: a Config Server that serves configuration from a backend (usually Git, or Vault, filesystem, S3), and Config Clients (the services) that fetch their configuration from the server on startup.

Server:

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
  public static void main(String[] args) {
    SpringApplication.run(ConfigServerApplication.class, args);
  }
}
# Config Server — serves from a Git repo
server:
  port: 8888
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/acme/config-repo
          default-label: main
          search-paths: '{application}'   # folder per service name

Files in the repo follow a naming convention: payment-service.yml, payment-service-prod.yml, and a global application.yml for values shared by all services.

The client: from bootstrap to spring.config.import

Important change: bootstrap is deprecated

In old Spring Cloud, the client connected to the Config Server with a bootstrap.yml and the spring-cloud-starter-bootstrap dependency. As of Spring Boot 2.4, the official default approach is spring.config.import. If you still see bootstrap.yml in a project, it's legacy. The modern way:

# client — application.yml
spring:
  application:
    name: payment-service
  config:
    import: "optional:configserver:http://localhost:8888"
  profiles:
    active: prod

optional: means that if the Config Server is unavailable, the app comes up with local config instead of crashing — good for dev. But in prod you might drop optional: so that if config doesn't arrive, the app deliberately refuses to start (fail-fast instead of booting with wrong config).

Profiles: one codebase, many environments

A profile is Spring's mechanism for "same app, different config per environment". With payment-service-prod.yml you layer prod-specific values over the base payment-service.yml. Precedence matters: more specific (profile-specific) values override more general ones, and environment variables / command-line arguments override all of them.

Live refresh: the heart of it

The whole point of centralized config is being able to change a setting without a restart. The mechanism:

  1. Mark the bean you want refreshed with @RefreshScope. On refresh, this bean is rebuilt and reads the new values.
  2. POST to /actuator/refresh on that instance. This publishes a RefreshScopeRefreshedEvent, and @RefreshScope beans are rebuilt and @ConfigurationProperties are rebound.
@RefreshScope
@Component
public class PricingConfig {
  @Value("${pricing.discount-percent:0}")
  private int discountPercent;   // new value after refresh

  public int discountPercent() { return discountPercent; }
}
# refreshes only this one instance
curl -X POST http://payment-1:8080/actuator/refresh

But there's a problem: if you have 20 instances of payment-service, do you curl 20 times? This is where Spring Cloud Bus comes in.

Spring Cloud Bus: refresh all instances at once

Spring Cloud Bus connects all instances through a message broker (Kafka or RabbitMQ) onto one shared "bus". A single POST to /actuator/busrefresh on one instance is enough: that instance publishes a RefreshRemoteApplicationEvent onto the broker, and all subscribed instances receive it and refresh themselves.

Broadcast config refresh over the bus / پخش refresh تنظیمات روی bus:

flowchart TD
  Ops[Ops: POST /actuator/busrefresh] --> S1[payment-1]
  S1 -->|publish RefreshRemoteApplicationEvent| Broker[(Kafka / RabbitMQ)]
  Broker --> S1
  Broker --> S2[payment-2]
  Broker --> S3[payment-3]
  S1 --> Cfg[(Config Server -> Git)]
  S2 --> Cfg
  S3 --> Cfg
# client with the bus on kafka
spring:
  cloud:
    bus:
      enabled: true
    stream:
      kafka:
        binder:
          brokers: kafka:9092
management:
  endpoints:
    web:
      exposure:
        include: busrefresh, refresh, health

A more mature architecture: a webhook from Git (on each push) hits the Config Server, which in turn triggers busrefresh — meaning a commit to the config repo automatically propagates to all services. True GitOps for config.

Not everything changes with a refresh

A common trap: you expect any setting to change live with busrefresh, but some things are only read at startup and refresh doesn't touch them — like DB connection-pool settings, the server port, or beans without @RefreshScope. Teams change a flag, hit busrefresh, and are baffled that nothing happened. The lesson: know exactly what is refreshable, test it, and for non-refreshable changes plan a rolling restart. A half-refreshed state (some instances new, some stale) is itself a dangerous condition you must be ready for.

Managing secrets

Putting a DB password as plain text in Git is a security disaster. Three approaches:

  1. Encryption in the Config Server: the Config Server can encrypt values; in the repo the value is stored as {cipher}AQB..., and the server decrypts it when serving. The /encrypt and /decrypt endpoints exist for this.
# in the repo, the password is stored encrypted
spring:
  datasource:
    password: '{cipher}AQBvJ8x...k9'
  1. HashiCorp Vault as the backend: instead of Git, you keep secrets in Vault, which provides rotation, time-bounded leases, and audit. Spring Cloud Config/Vault integrates it.

  2. Platform secrets (on k8s): you use a Kubernetes Secret (and, better, a solution like External Secrets Operator or Sealed Secrets), delivered to the Pod as env vars or mounted files. On k8s this is usually cleaner than hand-encrypting in Git.

Never commit a secret to Git in plain text — even a private repo

A dangerous misconception: "our config repo is private, so plain-text passwords are fine." Git history is forever; any secret committed once stays in history even after you delete it and must be rotated. On top of that, every developer who clones the repo has a copy of all prod secrets on their laptop. Seniors separate secrets from ordinary config (Vault or platform Secrets), minimize access, and run a secret scanner (like gitleaks) in CI.

Keep config separate from the domain

A simple senior rule: what varies between environments → config. What is domain behavior → code. If you constantly have to touch a config file for a behavior change, maybe that behavior belongs in code and tests, not config. Conversely, hardcoding addresses and limits in code means every change needs a redeploy. The right boundary between the two reveals a team's operational maturity.


Putting it all together: one full request

Let's trace a complete request from client to database and see where each of the three tools plays its part.

End-to-end request across all three pillars / یک درخواست سرتاسری از میان هر سه ستون:

sequenceDiagram
  participant C as Client
  participant G as Gateway
  participant R as Registry
  participant O as Order Service
  participant Cfg as Config Server
  C->>G: GET /api/orders (JWT)
  G->>G: Validate JWT, check rate limit
  G->>R: Resolve lb://order-service
  R-->>G: order-2 @ 10.1.4.7:8080
  G->>O: GET /orders (relay token)
  Note over O,Cfg: On startup O had pulled its config
  O->>O: Apply @RefreshScope values
  O-->>G: 200 orders
  G-->>C: 200 orders

The temporal order: the Config Server handed the service its config when it started; the service registered itself in the Registry; now the user's request reaches the Gateway, where it is authenticated and rate-limited; the Gateway asks the Registry for Order's live address, load-balances, relays the token, and the response comes back. This coordinated dance is what makes a microservice platform actually run.


Senior lens: anti-patterns and production lessons

Hidden distribution and the "distributed monolith"

The worst fate: you've split into services but they're so entangled that any change requires several coordinated deploys — the "distributed monolith", the worst of both worlds (distribution complexity + monolith coupling). Gateway, discovery, and config hide this coupling but don't cure it. Draw your service boundaries correctly (bounded contexts), or the tools of this chapter just become plumbing for a bad design.

Start simple, get complex in the right place

A maturity note: you don't need Eureka + Config Server + Bus + Vault + mesh on day one. If you're on Kubernetes, start with Service DNS for discovery and ConfigMap/Secret for config; add a gateway when you actually have multiple external services. Every piece of this stack is an operational component that must be monitored, patched, and on-call. Being senior means knowing what you don't need yet.

Observability before scale

When a request goes gateway → two services → database and slows down, without distributed tracing (like OpenTelemetry with a trace-id that starts at the gateway and propagates across every hop) you're blind. Teams that built first and added observability later burned for months in production incidents. The gateway is the natural place to mint the ingress trace-id — start there.

What's the difference between client-side and server-side discovery, and which do you pick?

In client-side (e.g. Eureka + Spring Cloud LoadBalancer) the consumer itself pulls the instance list from the registry and calls one directly; one fewer hop and algorithm control, but every client must carry a discovery library (painful for polyglot). In server-side (e.g. Kubernetes Service + kube-proxy, or an L7 LB) the client just hits one fixed name and the intermediary routes; the client stays simple but there's an extra hop and an extra infra component. My choice: on k8s almost always the platform's server-side (Service DNS), because a parallel discovery in the app is just a bug source; on VMs/legacy, Eureka makes sense.

What happened to Ribbon, and what replaces it?

Netflix Ribbon was Spring Cloud's old client-side load-balancing library and is now deprecated. The official successor is Spring Cloud LoadBalancer — lighter, with reactive support, and a core part of Spring Cloud. Its default algorithm is Round Robin, it also has a RandomLoadBalancer, it's enabled with @LoadBalanced on a WebClient/RestClient, and it has a configurable instance cache and retry. If I still see Ribbon in code, I treat it as a sign of legacy and a migration candidate.

What's the difference between a Route, a Predicate, and a Filter in Spring Cloud Gateway?

A Route is the routing unit: an id, a target uri, a set of predicates, and a set of filters. A Predicate is the match condition — does this request match this route? (by Path, Method, Header, Host, time…). A Filter is work performed on the matched request/response, pre (before forwarding, e.g. add a header or rate-limit) or post (after the response, e.g. add a response header). Put simply: the predicate decides "whether", the filter decides "what to do".

How do you implement rate limiting in Spring Cloud Gateway?

With the RequestRateLimiter filter and its default RedisRateLimiter, which uses the Token Bucket algorithm and needs spring-boot-starter-data-redis-reactive (Redis for state shared across gateway instances). Three key parameters: replenishRate (steady tokens/sec), burstCapacity (burst ceiling), and requestedTokens (cost per request). A KeyResolver bean decides what the rate counts against (user/IP/API-key). As of 2025.0 there's also a Bucket4jRateLimiter option. Senior note: explicitly decide the fail-open/fail-closed behavior when Redis is down.

Why shouldn't you put business logic in the gateway?

Because the gateway is a shared component on the critical path of all traffic; putting domain logic in it locks every team to one shared deployment point, ownership gets lost, and the gateway becomes a "hidden monolith". The gateway should only know cross-cutting concerns (auth, rate limit, routing, observability). If aggregation or client-specific shaping is needed, use the BFF pattern, which has a clear owner (that frontend's team) and a clear boundary.

What is a BFF and how does it differ from an API Gateway?

A BFF (Backend for Frontend) means each client type (mobile, web, partner) has a dedicated thin backend that optimizes aggregation and shaping for that client. The difference from a gateway: a gateway applies generic cross-cutting policy to all traffic and is domain-agnostic; a BFF knows one specific client's experience logic and is owned by that frontend's team. They're often used together: a gateway at the edge for auth/rate-limit, with several BFFs behind it. Watch out for BFF proliferation (one per page); split by client type.

What's the difference between Eureka and Consul?

Both are registries but with different philosophies. Eureka is heartbeat-based and in the AP camp of CAP (prefers availability over consistency; under a network partition it prefers to serve possibly-stale data rather than none) and has a self-preservation mechanism. Consul runs real health checks (HTTP/TCP/script), is Raft-based with stronger consistency (CP), and beyond discovery offers a KV store for config and polyglot support. For a purely Spring ecosystem, Eureka is simpler; for a multi-platform environment needing config and precise health, Consul.

How do you refresh config across all services without a restart?

Single instance: mark the bean @RefreshScope and POST /actuator/refresh, which publishes a RefreshScopeRefreshedEvent and rebuilds the beans. For all instances at once: Spring Cloud Bus over Kafka/RabbitMQ. A POST /actuator/busrefresh on one instance publishes a RefreshRemoteApplicationEvent onto the broker and everyone refreshes. The most mature setup: a webhook from Git triggers busrefresh on each push. But know that not everything is refreshable — the port, connection pool, and beans without @RefreshScope only change on restart.

Why is the gateway a SPOF, and how do you mitigate it?

Because all ingress traffic passes through it; its failure cuts off the whole platform from outside. Mitigation: keep the gateway stateless so it scales horizontally; run several instances behind an L4/L7 LB; set explicit timeouts and circuit breakers so a slow service doesn't swallow the gateway's connection pool and cause a cascading failure; proper health checks and graceful shutdown; and observability with a trace-id from the gateway itself. Philosophy: the gateway should be thin but highly resilient.

How do you manage secrets in a config platform?

Never plain text in Git — even a private repo, because history is forever and every clone is a copy of the secrets. Three approaches: (1) encryption in the Config Server with {cipher} and the encrypt/decrypt endpoints; (2) use HashiCorp Vault as the backend, which has rotation, leases, and audit; (3) on Kubernetes use platform Secrets (better with External Secrets/Sealed Secrets). Separate secrets from ordinary config, minimize access, and run a secret scanner in CI. If a secret leaks, rotation is mandatory — not just deleting the commit.

What does the lb:// prefix in a route's uri do?

It means the gateway, instead of connecting to a fixed host, resolves the service name after lb:// via Spring Cloud LoadBalancer and the registry (Eureka/Consul/k8s): it gets the list of healthy instances, picks one round-robin, and sends the request to it. This is exactly where all three pillars of the chapter meet — routing (Gateway) over discovery (Registry) with client-side load balancing. If you instead write http://host:port, the gateway connects directly with no discovery.

On Kubernetes, do you still need Eureka and Spring Cloud Config?

Often not entirely. Kubernetes natively provides discovery (Service + DNS + kube-proxy) and config (ConfigMap/Secret), language-agnostically, so adding Eureka is usually harmful redundancy. But Spring Cloud Config still adds value if you want live refresh without a rolling restart, Git history, centralized encryption, or a single source shared between k8s and non-k8s — because a changed ConfigMap doesn't reflect into the app without a restart by default (unless you add a helper). The mature answer: "I lean on the platform except where the Config Server gives a capability k8s doesn't easily provide."

How do you prevent cascading failure through the gateway?

With a combination of edge patterns: explicit timeouts (connect and response) so a slow service doesn't lock up the gateway's connections; a circuit breaker (like Resilience4j) that fails fast to a fallback when a service is broken instead of waiting; bounded, safe retries (only on idempotent calls); bulkheads to isolate the resource pool per target so one failure doesn't sink the rest; and rate limiting to prevent overload. The mental key: every bounded resource (thread, connection) must have a ceiling and a deadline; an unbounded resource is the agent of chain collapse.

Chapter capsule
  • The three operational pillars of microservices: Gateway (edge doorway), Service Discovery (live phone book), Config (control room for settings).
  • Discovery: client-side (Eureka + Spring Cloud LoadBalancer) vs server-side (Kubernetes Service/DNS). On k8s, usually lean on the platform. Eureka is AP with self-preservation; Consul does real health checks and is CP.
  • Load Balancing: Ribbon is deprecated; the successor is Spring Cloud LoadBalancer with round-robin and @LoadBalanced. Plain round-robin + health + circuit breaker usually beats a half-blind algorithm.
  • Gateway: Spring Cloud Gateway with Route/Predicate/Filter; new artifact names from 2025.0 (spring-cloud-starter-gateway-server-webflux); rate limiting with RedisRateLimiter (Token Bucket); edge auth with JWT and TokenRelay; lb:// to reach the registry. Keep the gateway thin, stateless, with timeouts and circuit breakers, and never put domain logic in it.
  • BFF: a tailored backend per client type; client-specific aggregation and shaping, not in the shared gateway.
  • Config: Spring Cloud Config with a Git backend; spring.config.import instead of bootstrap; profiles for environments; refresh with @RefreshScope + /actuator/refresh and global broadcast via Spring Cloud Bus (/actuator/busrefresh); secrets via Vault/encryption, never plain text in Git.
  • Senior judgment: start simple, add observability early, draw service boundaries right so you don't build a "distributed monolith", and protect every bounded resource with timeouts and circuit breakers.