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:
- config-server (port 8888) — serves central configs from a Git/native folder
- eureka-server (port 8761) — service registry
- inventory-service (port random) — upstream provider (simulated flakiness)
- pricing-service (port random) — upstream provider (simulated flakiness)
- order-service (port random) — aggregates inventory+pricing, protected by Resilience4j
- 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)
- Run Config Server (
config-server
) → http://localhost:8888/actuator/health - Run Eureka Server (
eureka-server
) → http://localhost:8761 - Run Inventory, Pricing, Order, API Gateway (order doesn’t need a fixed port)
- 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
- Inventory:
-
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)
- Config load order: In Spring Cloud 2023+, use
spring.config.import: optional:configserver:...
in application.yml (no separatebootstrap.yml
). - Eureka name vs. route: Service ID is
spring.application.name
. Gateway discovery locator uses lowercase IDs whenlowerCaseServiceId: true
. - @LoadBalanced WebClient: Add
spring-cloud-starter-loadbalancer
and@LoadBalanced WebClient.Builder
or RestClient/RestTemplate; URLs usehttp://service-id/...
. - Resilience4j fallback signature: Must match original method params plus a trailing
Throwable
. - Circuit Breaker states: CLOSED → OPEN after threshold; after
wait-duration-in-open-state
, HALF_OPEN allows limited probes. - Gateway timeouts vs client timeouts: Protect both gateway and callers; gateway’s
response-timeout
doesn’t replace downstream client timeouts. - Actuator exposure: For demos we expose
*
; in prod restrict it. - Statelessness: Without DB, ensure idempotency and keep state in the client or cache; avoids coupling and makes scaling trivial.
- Config refresh: With Spring Boot 3, use Spring Cloud 2023+; new refresh mechanism prefers
/actuator/refresh
withspring-cloud-starter-actuator
(or restart in dev). Avoid legacy@RefreshScope
pitfalls by scoping only beans that truly need hot reload. - 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 togit
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 forConfigDataLocationNotFoundException
. No qualifying bean: WebClient.Builder
→ Ensure@Bean @LoadBalanced WebClient.Builder
exists inorder-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)
- Create parent POM; add modules.
- Create
config-repo
+ YAMLs. - Build config-server (native backend to
../config-repo
), run. - Build eureka-server, run.
- Build inventory-service, pricing-service, order-service, api-gateway with given code.
- Start services; verify on
http://localhost:8761
. - Hit:
http://localhost:8080/orders/aggregate/ABC123?unstable=true
and observe fallbacks. - Add chaos with
delayMs=3000
and check timeouts.
Happy Learning