Thursday, 14 August 2025

Fault‑Tolerant Microservices with Eureka, Config Server & Gateway

 

Fault‑Tolerant Microservices  with Eureka, Config Server & Gateway

Goal: Build a production‑style microservices playground with zero databases using Spring Boot 3.3.x, Spring Cloud 2023.x, Eureka discovery, Spring Cloud Config Server, Spring Cloud Gateway, and Resilience4j. You’ll create five apps:

  1. config-server (port 8888) — serves central configs from a Git/native folder
  2. eureka-server (port 8761) — service registry
  3. inventory-service (port random) — upstream provider (simulated flakiness)
  4. pricing-service (port random) — upstream provider (simulated flakiness)
  5. order-service (port random) — aggregates inventory+pricing, protected by Resilience4j
  6. api-gateway (port 8080) — Spring Cloud Gateway front door (routes by serviceId)

No DB, all stateless services; configurable chaos via query params.


0) Prerequisites

  • JDK 17/21, Maven 3.9+, STS 4/IntelliJ
  • Use Spring Initializr defaults for Spring Boot 3.3.2 (or newer 3.3.x)

1) Project Structure (Maven Multi‑Module)

microservices-fault-tolerant-demo/
├─ pom.xml                       (parent)
├─ config-repo/                  (external config files, Git-able)
│  ├─ application.yml
│  ├─ api-gateway.yml
│  ├─ eureka-server.yml
│  ├─ inventory-service.yml
│  ├─ pricing-service.yml
│  └─ order-service.yml
├─ config-server/
├─ eureka-server/
├─ api-gateway/
├─ inventory-service/
├─ pricing-service/
└─ order-service/

You can keep config-repo as a plain folder in the parent project (Config Server native profile) while practicing. Later switch to a real Git repo.


2) Parent pom.xml

Create a root Maven project (packaging pom). Add Spring Boot & Spring Cloud BOMs so all modules share versions.

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>microservices-fault-tolerant-demo</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <properties>
    <java.version>17</java.version>
    <spring.boot.version>3.3.2</spring.boot.version>
    <spring.cloud.version>2023.0.3</spring.cloud.version>
    <resilience4j.version>2.2.0</resilience4j.version>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring.boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>${spring.cloud.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <modules>
    <module>config-server</module>
    <module>eureka-server</module>
    <module>api-gateway</module>
    <module>inventory-service</module>
    <module>pricing-service</module>
    <module>order-service</module>
  </modules>

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

3) Config Repository (external) — config-repo/*

Create files below (YAML per application name). Spring Cloud Config serves them as / {app-name}.yml.

3.1 config-repo/application.yml (shared defaults)

# Shared defaults for all services (overridden by each service yml)
management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

spring:
  cloud:
    loadbalancer:
      ribbon:
        enabled: false

3.2 config-repo/eureka-server.yml

server:
  port: 8761
spring:
  application:
    name: eureka-server

# Single-node, don't register itself
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
  server:
    enable-self-preservation: false

3.3 config-repo/api-gateway.yml

server:
  port: 8080
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lowerCaseServiceId: true
      default-filters:
        - RemoveRequestHeader=Cookie
        - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
      routes:
        # Optional explicit routes (discovery locator already works)
        - id: order
          uri: lb://order-service
          predicates:
            - Path=/orders/**
          filters:
            - StripPrefix=1

    # Global timeout to protect gateway
    httpclient:
      connect-timeout: 2000
      response-timeout: 3s

# CORS (relax for dev only)
management:
  endpoints:
    web:
      exposure:
        include: "*"

3.4 config-repo/inventory-service.yml

spring:
  application:
    name: inventory-service
server:
  port: 0 # random

# Custom props
inventory:
  failure-rate: 30   # % failures when unstable=true
  min-delay-ms: 50
  max-delay-ms: 250

# Register to Eureka
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

3.5 config-repo/pricing-service.yml

spring:
  application:
    name: pricing-service
server:
  port: 0

pricing:
  failure-rate: 25
  min-delay-ms: 50
  max-delay-ms: 300

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

3.6 config-repo/order-service.yml

spring:
  application:
    name: order-service
server:
  port: 0

# WebClient timeouts
http:
  client:
    connect-timeout-ms: 2000
    response-timeout-ms: 2000

# Resilience4j protections
resilience4j:
  circuitbreaker:
    instances:
      inventory:
        sliding-window-size: 10
        failure-rate-threshold: 50
        minimum-number-of-calls: 5
        wait-duration-in-open-state: 10s
        permitted-number-of-calls-in-half-open-state: 3
        automatic-transition-from-open-to-half-open-enabled: true
      pricing:
        sliding-window-size: 10
        failure-rate-threshold: 50
        minimum-number-of-calls: 5
        wait-duration-in-open-state: 10s
  retry:
    instances:
      inventory:
        max-attempts: 3
        wait-duration: 200ms
        enable-exponential-backoff: true
        exponential-backoff-multiplier: 2
      pricing:
        max-attempts: 3
        wait-duration: 200ms
        enable-exponential-backoff: true
        exponential-backoff-multiplier: 2
  ratelimiter:
    instances:
      aggregate:
        limit-for-period: 5
        limit-refresh-period: 1s
        timeout-duration: 0
  bulkhead:
    instances:
      aggregate:
        max-concurrent-calls: 20
        max-wait-duration: 0

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

4) Create Config Server (module: config-server)

Dependencies: Spring Web, Spring Cloud Config Server, Spring Boot Actuator

config-server/pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.example</groupId>
    <artifactId>microservices-fault-tolerant-demo</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>
  <artifactId>config-server</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  </dependencies>
</project>

config-server/src/main/java/.../ConfigServerApplication.java

package com.example.configserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
  public static void main(String[] args) {
    SpringApplication.run(ConfigServerApplication.class, args);
  }
}

config-server/src/main/resources/application.yml

server:
  port: 8888
spring:
  application:
    name: config-server
  profiles:
    active: native
  cloud:
    config:
      server:
        native:
          search-locations: file:../config-repo
management:
  endpoints:
    web:
      exposure:
        include: "*"

Run first so other services can fetch config.


5) Create Eureka Server (module: eureka-server)

Dependencies: Spring Web, Eureka Server, Spring Boot Actuator

eureka-server/pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.example</groupId>
    <artifactId>microservices-fault-tolerant-demo</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>
  <artifactId>eureka-server</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  </dependencies>
</project>

eureka-server/src/main/java/.../EurekaServerApplication.java

package com.example.eurekaserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
  public static void main(String[] args) {
    SpringApplication.run(EurekaServerApplication.class, args);
  }
}

eureka-server/src/main/resources/application.yml

spring:
  application:
    name: eureka-server

# Pull from Config Server
spring.config.import: optional:configserver:http://localhost:8888

Config for port & eureka flags comes from config-repo/eureka-server.yml.


6) Create API Gateway (module: api-gateway)

Dependencies: Spring Cloud Gateway, Eureka Client, Actuator

api-gateway/pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.example</groupId>
    <artifactId>microservices-fault-tolerant-demo</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>
  <artifactId>api-gateway</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  </dependencies>
</project>

api-gateway/src/main/java/.../ApiGatewayApplication.java

package com.example.apigateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ApiGatewayApplication {
  public static void main(String[] args) {
    SpringApplication.run(ApiGatewayApplication.class, args);
  }
}

api-gateway/src/main/resources/application.yml

spring:
  application:
    name: api-gateway
spring.config.import: optional:configserver:http://localhost:8888

7) Create Inventory Service (module: inventory-service)

Dependencies: Spring Web, Eureka Client, Actuator

inventory-service/pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.example</groupId>
    <artifactId>microservices-fault-tolerant-demo</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>
  <artifactId>inventory-service</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  </dependencies>
</project>

inventory-service/src/main/java/.../InventoryServiceApplication.java

package com.example.inventory;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class InventoryServiceApplication {
  public static void main(String[] args) {
    SpringApplication.run(InventoryServiceApplication.class, args);
  }
}

inventory-service/src/main/resources/application.yml

spring:
  application:
    name: inventory-service
spring.config.import: optional:configserver:http://localhost:8888

inventory-service/src/main/java/.../api/AvailabilityResponse.java

package com.example.inventory.api;

public class AvailabilityResponse {
  private String sku; private boolean available; private String source; private String message;
  public AvailabilityResponse() {}
  public AvailabilityResponse(String sku, boolean available, String source, String message) {
    this.sku = sku; this.available = available; this.source = source; this.message = message; }
  public String getSku() { return sku; } public void setSku(String s) { this.sku = s; }
  public boolean isAvailable() { return available; } public void setAvailable(boolean a) { this.available = a; }
  public String getSource() { return source; } public void setSource(String s) { this.source = s; }
  public String getMessage() { return message; } public void setMessage(String m) { this.message = m; }
}

inventory-service/src/main/java/.../api/InventoryController.java

package com.example.inventory.api;

import java.util.Random;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/inventory")
public class InventoryController {
  private final Random random = new Random();
  @Value("${inventory.failure-rate:30}") private int failureRate;
  @Value("${inventory.min-delay-ms:50}") private long minDelay;
  @Value("${inventory.max-delay-ms:250}") private long maxDelay;

  @GetMapping("/{sku}")
  public ResponseEntity<AvailabilityResponse> availability(@PathVariable String sku,
      @RequestParam(defaultValue = "false") boolean unstable,
      @RequestParam(defaultValue = "0") long delayMs) throws InterruptedException {

    if (delayMs > 0) Thread.sleep(delayMs);
    else if (unstable) Thread.sleep(minDelay + random.nextLong(maxDelay - minDelay + 1));

    if (unstable && random.nextInt(100) < failureRate) {
      throw new RuntimeException("Simulated inventory failure");
    }

    boolean available = random.nextBoolean();
    return ResponseEntity.ok(new AvailabilityResponse(sku, available, "inventory-service",
        "Inventory live response"));
  }
}

8) Create Pricing Service (module: pricing-service)

Dependencies: Spring Web, Eureka Client, Actuator

pricing-service/pom.xml — same as inventory (change artifactId)

pricing-service/src/main/java/.../PricingServiceApplication.java

package com.example.pricing;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class PricingServiceApplication {
  public static void main(String[] args) {
    SpringApplication.run(PricingServiceApplication.class, args);
  }
}

pricing-service/src/main/resources/application.yml

spring:
  application:
    name: pricing-service
spring.config.import: optional:configserver:http://localhost:8888

pricing-service/src/main/java/.../api/PriceResponse.java

package com.example.pricing.api;

public class PriceResponse {
  private String sku; private double price; private String currency; private String source; private String message;
  public PriceResponse() {}
  public PriceResponse(String sku, double price, String currency, String source, String message) {
    this.sku = sku; this.price = price; this.currency = currency; this.source = source; this.message = message; }
  public String getSku() { return sku; } public void setSku(String s) { this.sku = s; }
  public double getPrice() { return price; } public void setPrice(double p) { this.price = p; }
  public String getCurrency() { return currency; } public void setCurrency(String c) { this.currency = c; }
  public String getSource() { return source; } public void setSource(String s) { this.source = s; }
  public String getMessage() { return message; } public void setMessage(String m) { this.message = m; }
}

pricing-service/src/main/java/.../api/PricingController.java

package com.example.pricing.api;

import java.util.Random;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/pricing")
public class PricingController {
  private final Random random = new Random();
  @Value("${pricing.failure-rate:25}") private int failureRate;
  @Value("${pricing.min-delay-ms:50}") private long minDelay;
  @Value("${pricing.max-delay-ms:300}") private long maxDelay;

  @GetMapping("/{sku}")
  public ResponseEntity<PriceResponse> price(@PathVariable String sku,
      @RequestParam(defaultValue = "false") boolean unstable,
      @RequestParam(defaultValue = "0") long delayMs) throws InterruptedException {

    if (delayMs > 0) Thread.sleep(delayMs);
    else if (unstable) Thread.sleep(minDelay + random.nextLong(maxDelay - minDelay + 1));

    if (unstable && random.nextInt(100) < failureRate) {
      throw new RuntimeException("Simulated pricing failure");
    }

    double price = 100 + random.nextInt(900) + random.nextDouble();
    return ResponseEntity.ok(new PriceResponse(sku, price, "INR", "pricing-service",
        "Pricing live response"));
  }
}

9) Create Order Service (module: order-service)

Dependencies: Spring WebFlux, Eureka Client, Actuator, AOP, Resilience4j, LoadBalancer

order-service/pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.example</groupId>
    <artifactId>microservices-fault-tolerant-demo</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>
  <artifactId>order-service</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
      <groupId>io.github.resilience4j</groupId>
      <artifactId>resilience4j-spring-boot3</artifactId>
      <version>${resilience4j.version}</version>
    </dependency>
  </dependencies>
</project>

order-service/src/main/java/.../OrderServiceApplication.java

package com.example.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class OrderServiceApplication {
  public static void main(String[] args) {
    SpringApplication.run(OrderServiceApplication.class, args);
  }
}

order-service/src/main/resources/application.yml

spring:
  application:
    name: order-service
spring.config.import: optional:configserver:http://localhost:8888

9.1 Load‑Balanced WebClient config

order-service/src/main/java/.../config/WebClientConfig.java

package com.example.order.config;

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

@Configuration
public class WebClientConfig {
  @Value("${http.client.connect-timeout-ms:2000}") private int connectTimeoutMs;
  @Value("${http.client.response-timeout-ms:2000}") private int responseTimeoutMs;

  @Bean @LoadBalanced
  public WebClient.Builder loadBalancedBuilder() {
    HttpClient httpClient = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMs)
        .responseTimeout(Duration.ofMillis(responseTimeoutMs))
        .doOnConnected(conn -> conn
          .addHandlerLast(new ReadTimeoutHandler(responseTimeoutMs, TimeUnit.MILLISECONDS))
          .addHandlerLast(new WriteTimeoutHandler(responseTimeoutMs, TimeUnit.MILLISECONDS)));
    return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient));
  }
}

9.2 DTOs

order-service/src/main/java/.../api/dto/AvailabilityResponse.java

package com.example.order.api.dto;
public class AvailabilityResponse {
  private String sku; private boolean available; private String source; private String message;
  public String getSku() { return sku; } public void setSku(String s) { this.sku = s; }
  public boolean isAvailable() { return available; } public void setAvailable(boolean a) { this.available = a; }
  public String getSource() { return source; } public void setSource(String s) { this.source = s; }
  public String getMessage() { return message; } public void setMessage(String m) { this.message = m; }
}

order-service/src/main/java/.../api/dto/PriceResponse.java

package com.example.order.api.dto;
public class PriceResponse {
  private String sku; private double price; private String currency; private String source; private String message;
  public String getSku() { return sku; } public void setSku(String s) { this.sku = s; }
  public double getPrice() { return price; } public void setPrice(double p) { this.price = p; }
  public String getCurrency() { return currency; } public void setCurrency(String c) { this.currency = c; }
  public String getSource() { return source; } public void setSource(String s) { this.source = s; }
  public String getMessage() { return message; } public void setMessage(String m) { this.message = m; }
}

order-service/src/main/java/.../api/dto/AggregateResponse.java

package com.example.order.api.dto;

public class AggregateResponse {
  private String sku; private boolean available; private Double price; private String currency; private String message;
  public AggregateResponse() {}
  public AggregateResponse(String sku, boolean available, Double price, String currency, String message) {
    this.sku = sku; this.available = available; this.price = price; this.currency = currency; this.message = message; }
  public String getSku() { return sku; } public void setSku(String s) { this.sku = s; }
  public boolean isAvailable() { return available; } public void setAvailable(boolean a) { this.available = a; }
  public Double getPrice() { return price; } public void setPrice(Double p) { this.price = p; }
  public String getCurrency() { return currency; } public void setCurrency(String c) { this.currency = c; }
  public String getMessage() { return message; } public void setMessage(String m) { this.message = m; }
}

9.3 Service with Resilience4j

order-service/src/main/java/.../service/OrderAggregator.java

package com.example.order.service;

import com.example.order.api.dto.*;
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Service
public class OrderAggregator {
  private final WebClient.Builder lb;
  public OrderAggregator(WebClient.Builder lb) { this.lb = lb; }

  @CircuitBreaker(name = "inventory", fallbackMethod = "inventoryFallback")
  @Retry(name = "inventory")
  public AvailabilityResponse getAvailability(String sku, boolean unstable, Long delayMs) {
    return lb.build().get()
      .uri("http://inventory-service/api/v1/inventory/{sku}?unstable={u}&delayMs={d}", sku, unstable, delayMs == null ? 0 : delayMs)
      .accept(MediaType.APPLICATION_JSON)
      .retrieve()
      .bodyToMono(AvailabilityResponse.class)
      .block();
  }

  public AvailabilityResponse inventoryFallback(String sku, boolean unstable, Long delayMs, Throwable ex) {
    AvailabilityResponse r = new AvailabilityResponse();
    r.setSku(sku); r.setAvailable(false); r.setSource("order-service[fallback]");
    r.setMessage("Inventory fallback: " + ex.getClass().getSimpleName());
    return r;
  }

  @CircuitBreaker(name = "pricing", fallbackMethod = "pricingFallback")
  @Retry(name = "pricing")
  public PriceResponse getPrice(String sku, boolean unstable, Long delayMs) {
    return lb.build().get()
      .uri("http://pricing-service/api/v1/pricing/{sku}?unstable={u}&delayMs={d}", sku, unstable, delayMs == null ? 0 : delayMs)
      .accept(MediaType.APPLICATION_JSON)
      .retrieve()
      .bodyToMono(PriceResponse.class)
      .block();
  }

  public PriceResponse pricingFallback(String sku, boolean unstable, Long delayMs, Throwable ex) {
    PriceResponse r = new PriceResponse();
    r.setSku(sku); r.setPrice(null); r.setCurrency(null); r.setSource("order-service[fallback]");
    r.setMessage("Pricing fallback: " + ex.getClass().getSimpleName());
    return r;
  }

  @RateLimiter(name = "aggregate")
  @Bulkhead(name = "aggregate")
  public AggregateResponse aggregate(String sku, boolean unstable, Long delayMs) {
    AvailabilityResponse inv = getAvailability(sku, unstable, delayMs);
    PriceResponse price = getPrice(sku, unstable, delayMs);

    String msg = (inv.getMessage() != null ? inv.getMessage() : "") +
                 " | " + (price.getMessage() != null ? price.getMessage() : "");

    return new AggregateResponse(sku, inv.isAvailable(), price.getPrice(), price.getCurrency(), msg.trim());
  }
}

9.4 Controller

order-service/src/main/java/.../api/OrderController.java

package com.example.order.api;

import com.example.order.api.dto.AggregateResponse;
import com.example.order.service.OrderAggregator;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
  private final OrderAggregator svc;
  public OrderController(OrderAggregator svc) { this.svc = svc; }

  /** Aggregate upstreams with fault‑tolerance */
  @GetMapping("/aggregate/{sku}")
  public AggregateResponse aggregate(@PathVariable String sku,
                                     @RequestParam(defaultValue = "false") boolean unstable,
                                     @RequestParam(required = false) Long delayMs) {
    return svc.aggregate(sku, unstable, delayMs);
  }
}

10) Start‑up Order (Step‑by‑Step in STS)

  1. Run Config Server (config-server) → http://localhost:8888/actuator/health
  2. Run Eureka Server (eureka-server) → http://localhost:8761
  3. Run Inventory, Pricing, Order, API Gateway (order doesn’t need a fixed port)
  4. Watch instances appear on Eureka dashboard.

If a service can’t start: check it can reach Config Server and Eureka; the log will show Imported config from ... when OK.


11) Test Endpoints (through Gateway)

  • Gateway → Order aggregate:
    http://localhost:8080/orders/aggregate/ABC123
    http://localhost:8080/orders/aggregate/ABC123?unstable=true
    http://localhost:8080/orders/aggregate/ABC123?delayMs=3000

  • Direct services (for debugging):

    • Inventory: http://localhost:<inventoryPort>/api/v1/inventory/ABC123?unstable=true
    • Pricing: http://localhost:<pricingPort>/api/v1/pricing/ABC123?unstable=true
  • Actuator samples:

    • http://localhost:<orderPort>/actuator/metrics/resilience4j.circuitbreaker.calls
    • http://localhost:<orderPort>/actuator/health

Expected behavior:

  • With unstable=true, failures/timeouts trigger Retry then CircuitBreaker fallbacks.
  • With delayMs>2000, timeouts occur (configured in client), fallbacks return partial data.
  • RateLimiter/Bulkhead protect aggregate endpoint under load.

12) Tricky Points (great for interviews)

  1. Config load order: In Spring Cloud 2023+, use spring.config.import: optional:configserver:... in application.yml (no separate bootstrap.yml).
  2. Eureka name vs. route: Service ID is spring.application.name. Gateway discovery locator uses lowercase IDs when lowerCaseServiceId: true.
  3. @LoadBalanced WebClient: Add spring-cloud-starter-loadbalancer and @LoadBalanced WebClient.Builder or RestClient/RestTemplate; URLs use http://service-id/....
  4. Resilience4j fallback signature: Must match original method params plus a trailing Throwable.
  5. Circuit Breaker states: CLOSED → OPEN after threshold; after wait-duration-in-open-state, HALF_OPEN allows limited probes.
  6. Gateway timeouts vs client timeouts: Protect both gateway and callers; gateway’s response-timeout doesn’t replace downstream client timeouts.
  7. Actuator exposure: For demos we expose *; in prod restrict it.
  8. Statelessness: Without DB, ensure idempotency and keep state in the client or cache; avoids coupling and makes scaling trivial.
  9. Config refresh: With Spring Boot 3, use Spring Cloud 2023+; new refresh mechanism prefers /actuator/refresh with spring-cloud-starter-actuator (or restart in dev). Avoid legacy @RefreshScope pitfalls by scoping only beans that truly need hot reload.
  10. Self‑registration race: Start Eureka early; if clients start first, they retry and eventually register; logs may show temporary 503 on discovery.

13) One‑Shot Run with Maven (optional)

From the root, open 6 terminals and run:

# 1
mvn -q -pl config-server spring-boot:run
# 2
mvn -q -pl eureka-server spring-boot:run
# 3
mvn -q -pl inventory-service spring-boot:run
# 4
mvn -q -pl pricing-service spring-boot:run
# 5
mvn -q -pl order-service spring-boot:run
# 6
mvn -q -pl api-gateway spring-boot:run

14) Extras & Practice Ideas

  • Add Health‑based routing in Gateway using CircuitBreaker filter.
  • Add request hedging (two upstream calls race; pick fastest).
  • Push config-repo to a Git repo and switch Config Server to git backend.
  • Dockerize all modules with a docker-compose.yml and chaos testing (tc netem).
  • Add a second instance of inventory/pricing; watch client‑side load balancing.

15) FAQ / Quick Fixes

  • Service can’t fetch config → Config Server not started or wrong spring.config.import. Check logs for ConfigDataLocationNotFoundException.
  • No qualifying bean: WebClient.Builder → Ensure @Bean @LoadBalanced WebClient.Builder exists in order-service.
  • Fallback not called → Verify annotation name matches instance in YAML and method signatures.
  • Gateway 404 → Path must match route predicate. With discovery locator, route uses serviceId as first path segment unless explicit route configured.
  • Eureka shows DOWN → Enable Actuator health; ensure management endpoints exposed; check port randomization.

16) Copy‑Paste Checklist (STS)

  1. Create parent POM; add modules.
  2. Create config-repo + YAMLs.
  3. Build config-server (native backend to ../config-repo), run.
  4. Build eureka-server, run.
  5. Build inventory-service, pricing-service, order-service, api-gateway with given code.
  6. Start services; verify on http://localhost:8761.
  7. Hit: http://localhost:8080/orders/aggregate/ABC123?unstable=true and observe fallbacks.
  8. Add chaos with delayMs=3000 and check timeouts.

Happy Learning 

No comments:

Post a Comment