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 

Monday, 21 July 2025

Difference between jakarta.validation-api & spring-boot-starter-validation

 

🔹 1. jakarta.validation-api

This provides the standard interface (API) for bean validation.
It includes annotations like:


@NotNull @Size(min = 3, max = 10) @Email @Min(1) @Max(100)

Dependency:


<dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> </dependency>

🔸 But: this is only the API, not the implementation.
You still need something to run these validations (see below).


🔹 2. spring-boot-starter-validation

This is a Spring Boot starter that:

  • Automatically includes the jakarta.validation-api

  • Adds the actual implementation (Hibernate Validator)

  • Integrates validation into Spring Boot (like @Valid in controllers)

Dependency:


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

✅ So Which One Should You Use?

You should only use:


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

Because:

  • It already includes jakarta.validation-api

  • It also brings in Hibernate Validator

  • Works perfectly with Spring Boot MVC / REST with @Valid


✨ Example in Spring Boot:


public class UserDTO { @NotNull @Size(min = 3, max = 20) private String name; @Email private String email; }

In controller:


@PostMapping("/user") public ResponseEntity<String> createUser
(@Valid @RequestBody UserDTO userDto) { return ResponseEntity.ok("Valid user!"); }


-----------------Difference----------


🎯 Use in Real Projects

In Spring Boot apps, always use:


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

It brings:

  • Jakarta API (@NotNull, etc.)

  • Hibernate Validator engine

  • Spring integration (works with @Valid in controllers)


🧠 Tricky Interview Q&A

1. Q: If I only add jakarta.validation-api to my pom.xml, will validation work?

👉 A: No, because jakarta.validation-api only provides interfaces and annotations.

You still need an implementation (like Hibernate Validator).


2. Q: What's the benefit of using spring-boot-starter-validation over directly

adding jakarta.validation-api and Hibernate Validator?

👉 A: spring-boot-starter-validation bundles everything you need and

auto-configures it for Spring Boot. Less config, more productivity.


3. Q: Can I use Jakarta Validation in non-Spring Java applications?

👉 A: Yes, but you must manually include both jakarta.validation-api and

an implementation like Hibernate Validator.


4. Q: What happens behind the scenes when I annotate a DTO with @Valid

in Spring Boot?

👉 A: Spring uses the Hibernate Validator (from the starter) to validate the

fields based on annotations. If validation fails,

a MethodArgumentNotValidException is thrown and handled globally.


5. Q: Which validation provider is included by default in Spring Boot

Starter Validation?

👉 A: Hibernate Validator.


Happy Learning... :)


Thursday, 17 July 2025

Solving the N+1 Query Problem in REST APIs: A Real-World Banking Example (Made Easy for Spring Boot Developers)

If you're using Spring Boot with Hibernate (JPA) and a relational database like MySQL/PostgreSQL, the N+1 Query Problem can silently hurt your API performance.

Let’s understand this issue in simple steps using a real-world banking project example, and how to fix it using Spring Boot tricks.


📌 What is the N+1 Problem?

The N+1 problem occurs when:

  • 1 SQL query fetches the main (parent) data — for example, bank accounts.

  • Then N additional SQL queries are executed for related (child) data — like each account’s transactions.

Expected: 1 smart query to fetch everything. ❌ Actual: 1 query for accounts + N queries for transactions = N+1 total queries.

This increases database load and slows down your REST APIs.


🏦 Spring Boot Banking API Example

Say you build a banking dashboard endpoint:

@GetMapping("/api/accounts")
public List<AccountDTO> getAllAccounts() {
    List<Account> accounts = accountRepository.findAll();
    return accounts.stream()
        .map(account -> new AccountDTO(account.getId(), account.getTransactions()))
        .collect(Collectors.toList());
}

Behind the scenes:

  • findAll() runs: SELECT * FROM account;

  • Then for each account, Hibernate runs: SELECT * FROM transaction WHERE account_id = ?;

➡️ With 100 accounts, this results in 101 queries. That’s the N+1 problem.


Why Is It a Big Deal?

  • 🚫 Bad for performance: 100 queries slow down response time.

  • 🔥 Heavy DB load: Affects scaling when users grow.

  • 🧪 Hard to debug: Looks fine in code, but performs poorly in production.


How to Fix N+1 in Spring Boot (Step-by-Step)

✅ 1. Use JOIN FETCH in JPA Query

@Query("SELECT a FROM Account a JOIN FETCH a.transactions")
List<Account> findAllWithTransactions();

✅ Fetches everything in 1 query.

✅ 2. Use JPA Entity Graphs

@EntityGraph(attributePaths = {"transactions"})
List<Account> findAll();

✅ Cleaner and better for dynamic loading.

✅ 3. Use DTO Projection (Recommended for REST)

@Query("SELECT new com.bank.dto.AccountDTO(a.id, t) 
FROM Account a JOIN a.transactions t")
List<AccountDTO> getAccountDTOs();

✅ Loads only the required data — no extra queries.

✅ 4. Use Batch Fetching (Optional)

In application.properties:

spring.jpa.properties.hibernate.default_batch_fetch_size=100

✅ Tells Hibernate to fetch child records in batches.


💡 Best Practices & Tricky Interview Points

  • 🧵 Lazy vs Eager:

    • Use LAZY loading to avoid unnecessary data.

    • Use JOIN FETCH when you really need the related data.

  • 🛡️ Avoid LazyInitializationException:

    • Use @Transactional in service layer if accessing lazy fields.

  • 🧪 Testing:

    • Use Hibernate query counters in tests to catch N+1 bugs.

    • Enable SQL logging: spring.jpa.show-sql=true

  • 🧠 Interview Tip:

    • Be ready to explain how you identify and fix N+1 using Spring Boot tools.


🚀 Summary: Fixing N+1 in Spring Boot

Problem Fix
Too many queries Use JOIN FETCH
Lazy loading issues Use EntityGraph or @Transactional
Poor API performance Use DTO projections
Query explosion Use batch fetch settings

🧠 Final Thought

"The fastest code isn’t the one that runs first — it’s the one that hits the DB the least.”

If you're building REST APIs for enterprise, finance, or dashboards — fixing N+1 makes your system faster, lighter, and scalable.

Happy Learning :) 


Sunday, 13 July 2025

JWT Interview Q & A asked in MNC company

 

🔐 1. How does Spring Security integrate with JWT-based authentication?

Answer:
Spring Security provides a flexible filter chain where you can plug in your own OncePerRequestFilter (e.g., JwtAuthenticationFilter) to intercept incoming requests and:

  • Extract the JWT from the Authorization header

  • Validate it

  • Set the Authentication object in SecurityContextHolder

➡️ This allows stateless authentication using JWT.


🔐 2. Why do we use OncePerRequestFilter in JWT security setup?

Answer:
OncePerRequestFilter ensures your filter runs only once per request, making it ideal for:

  • Checking JWT

  • Verifying token signature

  • Setting authentication context

If used wrongly, it can cause repeated token processing.


🔐 3. Can we disable session creation in Spring Security for JWT?

Answer:
Yes. JWT is stateless, so we must disable session creation:


http.sessionManagement()

    .sessionCreationPolicy(SessionCreationPolicy.STATELESS);


Without this, Spring may still create sessions and defeat the purpose of using JWT.


🔐 4. Where do you validate the JWT token in Spring Boot?

Answer:
In a custom JWT filter, before the request reaches the controller:

  • You extract the token from Authorization header

  • Validate the signature and expiration

  • Fetch user details (optional)

  • Set Authentication in SecurityContext


🔐 5. What happens if the JWT token is expired?

Answer:
When a JWT token is expired, it throws a specific exception — most commonly:


io.jsonwebtoken.ExpiredJwtException


In your custom JWT filter (usually extending OncePerRequestFilter), you should catch this exception and return a proper HTTP response:


try {

    // validate token logic

} catch (ExpiredJwtException e) {

    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

    response.getWriter().write("Token expired. Please login again or use refresh token.");

    return;

}


📌 HTTP Response:


401 Unauthorized  

Body: Token expired. Please login again or use refresh token.


🔄 Best Practice: Use a refresh token mechanism so that expired tokens can be refreshed without forcing the user to re-login every time.


🔐 6. How do you handle JWT in microservices architecture with Spring Security?

Answer:
In microservices, the best approach is to delegate all authentication to a central Auth Service. Here's the flow:

  1. User logs in via Auth Service → receives access token and refresh token.

  2. Access token is then passed in the Authorization: Bearer <token> header for every request to other services.

  3. Each service:

    • Validates the token using a shared secret or public key (if using asymmetric signing like RS256).

    • Authorizes the request based on roles in token claims.

  4. No session state is stored; each service is stateless.

Optional best practice:

  • Use Spring Cloud Gateway or an API Gateway to centralize token validation so individual services don't have to repeat it.


🔐 7. Is it safe to keep user roles in JWT?

Answer:
Yes — if the JWT is signed properly using a secure algorithm (HS256, RS256) and a strong secret/private key.

You can embed roles in the JWT payload like this:


{

  "sub": "user1",

  "roles": ["ROLE_ADMIN", "ROLE_USER"],

  "exp": 1723489234

}


📌 In your Spring Boot filter, you extract these roles and convert them to GrantedAuthority:


UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(

    username, null, authoritiesFromToken

);


⚠️ Do not trust claims blindly if the signature is invalid or token is tampered.

✅ Signed tokens = safe to use for roles
❌ Unsigned (alg: none) or weak key = huge security risk


🔐 8. What is the best way to store JWT tokens on the frontend?

Answer:

  • Access token: HttpOnly Secure Cookie (to avoid XSS)

  • Refresh token: also in HttpOnly cookie or encrypted local storage

❌ Avoid storing in localStorage or sessionStorage — vulnerable to XSS attacks.


🔐 9. Can we use Spring Security with both Session and JWT?

Answer:
Not advisable. Choose one:

  • Use Session + CSRF for monoliths or UI-driven apps

  • Use JWT (stateless) for REST APIs and microservices

Mixing both often leads to confusion and security holes.


🔐 10. How do you implement role-based access with JWT + Spring Security?

Answer:

  1. Encode roles into JWT claims.

  2. In your filter, extract and set GrantedAuthority in Authentication.

  3. Then use:

@PreAuthorize("hasRole('ADMIN')")


or


http.authorizeHttpRequests().requestMatchers("/admin/**").hasRole("ADMIN")


✅ Don’t forget to prefix roles with "ROLE_".