← Back to Advanced Java
Practical Work 5

Microservices Architecture

Build a product catalog system with two communicating microservices

Duration5-6 hours
DifficultyAdvanced
Session5 - Microservices Architecture

What You Will Learn

In this practical work, you'll build a simple microservices system from scratch. You'll understand:

  • What microservices are - Small, independent services that work together
  • Service communication - How services talk to each other over HTTP
  • Service discovery - How services find each other (Eureka)
  • API Gateway - A single entry point for all services
  • Resilience - Handling failures gracefully (Circuit Breaker)

Understanding Microservices

Monolith vs Microservices

Monolith: One big application with everything inside. Like a house where kitchen, bedroom, and bathroom are all connected - if you renovate the kitchen, you might affect the bathroom.

Microservices: Many small applications that communicate. Like apartments in a building - each is independent. You can renovate one without affecting others.

What we'll build:


   User Request
        │
        ▼
   ┌─────────────┐
   │ API Gateway │   ← Single entry point (port 8080)
   │  (Gateway)  │
   └─────────────┘
        │
   ┌────┴────┐
   │         │
   ▼         ▼
┌────────┐ ┌────────────┐
│Product │ │ Inventory  │   ← Independent services
│Service │ │  Service   │
│ :8081  │ │   :8082    │
└────────┘ └────────────┘
   │             │
   └──────┬──────┘
          │
   ┌──────────────┐
   │Eureka Server │   ← Service registry (port 8761)
   │   (Discovery)│       "Phone book" of services
   └──────────────┘

Prerequisites

  • Java 17+ and Maven installed
  • Understanding of REST APIs (Practical Work 4)
  • Completed Practical Work 3 (JPA basics)

Part 1: Create the Parent Project

Step 1.1: Project Structure

We'll create a multi-module Maven project. Create this folder structure:

product-catalog/
├── pom.xml                    (parent POM)
├── eureka-server/             (service discovery)
│   ├── pom.xml
│   └── src/main/...
├── product-service/           (manages products)
│   ├── pom.xml
│   └── src/main/...
├── inventory-service/         (manages stock)
│   ├── pom.xml
│   └── src/main/...
└── api-gateway/               (single entry point)
    ├── pom.xml
    └── src/main/...

Step 1.2: Create Parent pom.xml

Create product-catalog/pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<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>

    <!-- (#1:Parent manages versions for all child modules) -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.3</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>product-catalog</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>  <!-- (#2:pom = this is a parent) -->

    <!-- (#3:List all child modules) -->
    <modules>
        <module>eureka-server</module>
        <module>product-service</module>
        <module>inventory-service</module>
        <module>api-gateway</module>
    </modules>

    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2023.0.0</spring-cloud.version>
    </properties>

    <!-- (#4:Spring Cloud dependency management) -->
    <dependencyManagement>
        <dependencies>
            <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>
</project>
What is Spring Cloud?

Spring Cloud is a collection of tools for building microservices:

  • Eureka: Service discovery (like a phone book)
  • Gateway: Single entry point for all services
  • OpenFeign: Easy HTTP calls between services
  • Resilience4j: Handle failures gracefully

Part 2: Create Eureka Server (Service Discovery)

What is Service Discovery?

Imagine you're in a new city trying to find a restaurant. You could:

  1. Know the exact address (hard-coded URLs) - but what if they move?
  2. Ask a local guide who knows all restaurants (Service Discovery!)

Eureka is that guide. Services register with Eureka, and other services ask Eureka: "Where is the product-service?"

Step 2.1: Create eureka-server/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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>product-catalog</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>eureka-server</artifactId>

    <dependencies>
        <!-- (#1:Eureka Server dependency) -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

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

Step 2.2: Create Main Application

Create eureka-server/src/main/java/com/example/eureka/EurekaServerApplication.java:

package com.example.eureka;

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

@SpringBootApplication
@EnableEurekaServer  // (#1:This annotation makes this a Eureka Server!)
public class EurekaServerApplication {

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

Step 2.3: Configure Eureka Server

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

server:
  port: 8761  # (#1:Standard Eureka port)

eureka:
  client:
    register-with-eureka: false  # (#2:Don't register itself)
    fetch-registry: false         # (#3:Don't fetch from itself)

spring:
  application:
    name: eureka-server
Why .yml instead of .properties?

YAML is more readable for nested configurations. Both work equally well!

Step 2.4: Test Eureka Server

cd product-catalog/eureka-server
mvn spring-boot:run

Open http://localhost:8761 - you'll see the Eureka dashboard. The "Instances currently registered" section will be empty (no services yet).

Part 3: Create Product Service

Step 3.1: Create product-service/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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>product-catalog</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>product-service</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- (#1:Eureka Client - registers with Eureka Server) -->
        <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>

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

Step 3.2: Create Product Entity

Create product-service/src/main/java/com/example/product/entity/Product.java:

package com.example.product.entity;

import jakarta.persistence.*;
import java.math.BigDecimal;

@Entity
@Table(name = "products")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    private String description;

    @Column(nullable = false)
    private BigDecimal price;

    @Column(unique = true, nullable = false)
    private String sku;  // (#1:Stock Keeping Unit - unique product code)

    // Constructors
    public Product() {}

    public Product(String name, String description, BigDecimal price, String sku) {
        this.name = name;
        this.description = description;
        this.price = price;
        this.sku = sku;
    }

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }

    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }

    public String getSku() { return sku; }
    public void setSku(String sku) { this.sku = sku; }
}

Step 3.3: Create Repository

Create product-service/src/main/java/com/example/product/repository/ProductRepository.java:

package com.example.product.repository;

import com.example.product.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface ProductRepository extends JpaRepository<Product, Long> {
    Optional<Product> findBySku(String sku);
}

Step 3.4: Create DTO

Create product-service/src/main/java/com/example/product/dto/ProductResponse.java:

package com.example.product.dto;

import com.example.product.entity.Product;
import java.math.BigDecimal;

public class ProductResponse {
    private Long id;
    private String name;
    private String description;
    private BigDecimal price;
    private String sku;
    private Integer stockQuantity;  // (#1:Will be filled from Inventory Service)
    private boolean inStock;

    public static ProductResponse fromEntity(Product product) {
        ProductResponse response = new ProductResponse();
        response.id = product.getId();
        response.name = product.getName();
        response.description = product.getDescription();
        response.price = product.getPrice();
        response.sku = product.getSku();
        return response;
    }

    // Getters and Setters
    public Long getId() { return id; }
    public String getName() { return name; }
    public String getDescription() { return description; }
    public BigDecimal getPrice() { return price; }
    public String getSku() { return sku; }

    public Integer getStockQuantity() { return stockQuantity; }
    public void setStockQuantity(Integer stockQuantity) {
        this.stockQuantity = stockQuantity;
        this.inStock = stockQuantity != null && stockQuantity > 0;
    }

    public boolean isInStock() { return inStock; }
}

Step 3.5: Create Controller

Create product-service/src/main/java/com/example/product/controller/ProductController.java:

package com.example.product.controller;

import com.example.product.dto.ProductResponse;
import com.example.product.entity.Product;
import com.example.product.repository.ProductRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductRepository productRepository;

    public ProductController(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @GetMapping
    public List<ProductResponse> getAllProducts() {
        return productRepository.findAll()
                .stream()
                .map(ProductResponse::fromEntity)
                .toList();
    }

    @GetMapping("/{id}")
    public ResponseEntity<ProductResponse> getProductById(@PathVariable Long id) {
        return productRepository.findById(id)
                .map(product -> ResponseEntity.ok(ProductResponse.fromEntity(product)))
                .orElse(ResponseEntity.notFound().build());
    }

    @GetMapping("/sku/{sku}")
    public ResponseEntity<ProductResponse> getProductBySku(@PathVariable String sku) {
        return productRepository.findBySku(sku)
                .map(product -> ResponseEntity.ok(ProductResponse.fromEntity(product)))
                .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ProductResponse createProduct(@RequestBody Product product) {
        Product saved = productRepository.save(product);
        return ProductResponse.fromEntity(saved);
    }
}

Step 3.6: Create Main Application

Create product-service/src/main/java/com/example/product/ProductServiceApplication.java:

package com.example.product;

import com.example.product.entity.Product;
import com.example.product.repository.ProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.math.BigDecimal;

@SpringBootApplication
public class ProductServiceApplication {

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

    // (#1:Load sample data on startup)
    @Bean
    CommandLineRunner loadData(ProductRepository repository) {
        return args -> {
            repository.save(new Product("Laptop", "High-performance laptop", new BigDecimal("999.99"), "LAPTOP-001"));
            repository.save(new Product("Mouse", "Wireless mouse", new BigDecimal("29.99"), "MOUSE-001"));
            repository.save(new Product("Keyboard", "Mechanical keyboard", new BigDecimal("79.99"), "KEYBOARD-001"));
            System.out.println("Sample products loaded!");
        };
    }
}

Step 3.7: Configure Product Service

Create product-service/src/main/resources/application.yml:

server:
  port: 8081  # (#1:Product service runs on 8081)

spring:
  application:
    name: product-service  # (#2:Name used to register with Eureka)
  datasource:
    url: jdbc:h2:mem:productdb
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create-drop
  h2:
    console:
      enabled: true
      path: /h2-console

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/  # (#3:Where to find Eureka Server)

Step 3.8: Test Product Service

Keep Eureka Server running, then in a new terminal:

cd product-catalog/product-service
mvn spring-boot:run

Check Eureka dashboard at http://localhost:8761 - you should see PRODUCT-SERVICE registered!

Test the API:

curl http://localhost:8081/api/products

Part 4: Create Inventory Service

Step 4.1: Create inventory-service/pom.xml

Same dependencies as product-service. Create inventory-service/pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<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>product-catalog</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.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </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>

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

Step 4.2: Create Inventory Entity

Create inventory-service/src/main/java/com/example/inventory/entity/Inventory.java:

package com.example.inventory.entity;

import jakarta.persistence.*;

@Entity
@Table(name = "inventory")
public class Inventory {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String sku;  // (#1:Links to product's SKU)

    @Column(nullable = false)
    private Integer quantity;

    // Constructors
    public Inventory() {}

    public Inventory(String sku, Integer quantity) {
        this.sku = sku;
        this.quantity = quantity;
    }

    // Getters and Setters
    public Long getId() { return id; }
    public String getSku() { return sku; }
    public void setSku(String sku) { this.sku = sku; }
    public Integer getQuantity() { return quantity; }
    public void setQuantity(Integer quantity) { this.quantity = quantity; }
}

Step 4.3: Create Repository and DTO

Create inventory-service/src/main/java/com/example/inventory/repository/InventoryRepository.java:

package com.example.inventory.repository;

import com.example.inventory.entity.Inventory;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface InventoryRepository extends JpaRepository<Inventory, Long> {
    Optional<Inventory> findBySku(String sku);
}

Create inventory-service/src/main/java/com/example/inventory/dto/InventoryResponse.java:

package com.example.inventory.dto;

public class InventoryResponse {
    private String sku;
    private Integer quantity;
    private boolean inStock;

    public InventoryResponse(String sku, Integer quantity) {
        this.sku = sku;
        this.quantity = quantity;
        this.inStock = quantity > 0;
    }

    public String getSku() { return sku; }
    public Integer getQuantity() { return quantity; }
    public boolean isInStock() { return inStock; }
}

Step 4.4: Create Controller

Create inventory-service/src/main/java/com/example/inventory/controller/InventoryController.java:

package com.example.inventory.controller;

import com.example.inventory.dto.InventoryResponse;
import com.example.inventory.repository.InventoryRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/inventory")
public class InventoryController {

    private final InventoryRepository inventoryRepository;

    public InventoryController(InventoryRepository inventoryRepository) {
        this.inventoryRepository = inventoryRepository;
    }

    // (#1:This endpoint will be called by the API Gateway)
    @GetMapping("/{sku}")
    public ResponseEntity<InventoryResponse> getInventory(@PathVariable String sku) {
        return inventoryRepository.findBySku(sku)
                .map(inv -> ResponseEntity.ok(new InventoryResponse(inv.getSku(), inv.getQuantity())))
                .orElse(ResponseEntity.ok(new InventoryResponse(sku, 0)));  // (#2:Return 0 if not found)
    }
}

Step 4.5: Create Main Application

Create inventory-service/src/main/java/com/example/inventory/InventoryServiceApplication.java:

package com.example.inventory;

import com.example.inventory.entity.Inventory;
import com.example.inventory.repository.InventoryRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class InventoryServiceApplication {

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

    @Bean
    CommandLineRunner loadData(InventoryRepository repository) {
        return args -> {
            repository.save(new Inventory("LAPTOP-001", 10));
            repository.save(new Inventory("MOUSE-001", 50));
            repository.save(new Inventory("KEYBOARD-001", 0));  // (#1:Out of stock!)
            System.out.println("Sample inventory loaded!");
        };
    }
}

Step 4.6: Configure Inventory Service

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

server:
  port: 8082  # (#1:Inventory service runs on 8082)

spring:
  application:
    name: inventory-service
  datasource:
    url: jdbc:h2:mem:inventorydb
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create-drop
  h2:
    console:
      enabled: true
      path: /h2-console

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

Step 4.7: Test Inventory Service

In a new terminal:

cd product-catalog/inventory-service
mvn spring-boot:run

Check Eureka dashboard - you should now see both PRODUCT-SERVICE and INVENTORY-SERVICE!

curl http://localhost:8082/api/inventory/LAPTOP-001
# Returns: {"sku":"LAPTOP-001","quantity":10,"inStock":true}

Part 5: Create API Gateway

What is an API Gateway?

Instead of clients knowing about multiple services (product at :8081, inventory at :8082), they only talk to the Gateway at :8080. The Gateway routes requests to the right service.

Benefits: Single entry point, can add authentication, rate limiting, logging in one place.

Step 5.1: Create api-gateway/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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>product-catalog</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>api-gateway</artifactId>

    <dependencies>
        <!-- (#1:Spring Cloud Gateway) -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- (#2:Eureka Client to discover services) -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- (#3:Circuit Breaker for resilience) -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

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

Step 5.2: Create Main Application

Create api-gateway/src/main/java/com/example/gateway/ApiGatewayApplication.java:

package com.example.gateway;

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);
    }
}

Step 5.3: Configure Gateway Routes

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

server:
  port: 8080  # (#1:Gateway is the main entry point)

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true  # (#2:Enable service discovery routing)
          lower-case-service-id: true
      routes:
        # (#3:Route /products/** to product-service)
        - id: product-service
          uri: lb://product-service  # (#4:lb = load balanced via Eureka)
          predicates:
            - Path=/api/products/**

        # (#5:Route /inventory/** to inventory-service)
        - id: inventory-service
          uri: lb://inventory-service
          predicates:
            - Path=/api/inventory/**

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

# (#6:Enable actuator endpoints for monitoring)
management:
  endpoints:
    web:
      exposure:
        include: health,info,gateway
Understanding the Routes
  • uri: lb://product-service - "lb" means load-balanced; Eureka provides the actual URL
  • predicates - Conditions that must match for routing
  • Path=/api/products/** - All requests starting with /api/products go to product-service

Step 5.4: Test the Gateway

Start the gateway (keep other services running):

cd product-catalog/api-gateway
mvn spring-boot:run

Now test through the gateway (port 8080, not 8081):

# Get products through gateway
curl http://localhost:8080/api/products

# Get inventory through gateway
curl http://localhost:8080/api/inventory/LAPTOP-001

All requests go through port 8080, but are routed to the correct service!

Part 6: Add Circuit Breaker (Resilience)

What is a Circuit Breaker?

Like an electrical circuit breaker in your home. If there's a problem (service is down), the breaker "trips" and stops sending requests. This prevents cascading failures.

States:

  • CLOSED: Normal operation, requests flow through
  • OPEN: Service is failing, return fallback immediately
  • HALF-OPEN: Test if service recovered

Step 6.1: Add Fallback Controller

Create api-gateway/src/main/java/com/example/gateway/controller/FallbackController.java:

package com.example.gateway.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;

@RestController
@RequestMapping("/fallback")
public class FallbackController {

    // (#1:Called when product-service is down)
    @GetMapping("/products")
    public ResponseEntity<Map<String, String>> productServiceFallback() {
        return ResponseEntity
                .status(HttpStatus.SERVICE_UNAVAILABLE)
                .body(Map.of(
                        "message", "Product service is currently unavailable",
                        "status", "SERVICE_DOWN"
                ));
    }

    // (#2:Called when inventory-service is down)
    @GetMapping("/inventory")
    public ResponseEntity<Map<String, String>> inventoryServiceFallback() {
        return ResponseEntity
                .status(HttpStatus.SERVICE_UNAVAILABLE)
                .body(Map.of(
                        "message", "Inventory service is currently unavailable",
                        "status", "SERVICE_DOWN"
                ));
    }
}

Step 6.2: Update Gateway Configuration

Update api-gateway/src/main/resources/application.yml to add circuit breaker:

server:
  port: 8080

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/api/products/**
          filters:
            # (#1:Add circuit breaker filter)
            - name: CircuitBreaker
              args:
                name: productCircuitBreaker
                fallbackUri: forward:/fallback/products

        - id: inventory-service
          uri: lb://inventory-service
          predicates:
            - Path=/api/inventory/**
          filters:
            - name: CircuitBreaker
              args:
                name: inventoryCircuitBreaker
                fallbackUri: forward:/fallback/inventory

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

# (#2:Circuit breaker configuration)
resilience4j:
  circuitbreaker:
    instances:
      productCircuitBreaker:
        slidingWindowSize: 10  # (#3:Number of calls to track)
        failureRateThreshold: 50  # (#4:Open circuit if 50% fail)
        waitDurationInOpenState: 10000  # (#5:Wait 10s before trying again)
      inventoryCircuitBreaker:
        slidingWindowSize: 10
        failureRateThreshold: 50
        waitDurationInOpenState: 10000

management:
  endpoints:
    web:
      exposure:
        include: health,info,gateway

Step 6.3: Test Circuit Breaker

1. Stop the product-service (Ctrl+C in its terminal)

2. Try to access products through gateway:

curl http://localhost:8080/api/products

Expected response (fallback):

{
  "message": "Product service is currently unavailable",
  "status": "SERVICE_DOWN"
}

3. Restart product-service and try again - it works!

Deliverables Checklist

  • Eureka Server runs on port 8761
  • Product Service registers with Eureka (port 8081)
  • Inventory Service registers with Eureka (port 8082)
  • API Gateway routes requests (port 8080)
  • Gateway routes /api/products/** to product-service
  • Gateway routes /api/inventory/** to inventory-service
  • Circuit breaker returns fallback when service is down
  • All 4 services visible in Eureka dashboard

Running All Services

You need 4 terminal windows. Start in this order:

# Terminal 1: Eureka Server (start first, wait for it to be ready)
cd product-catalog/eureka-server && mvn spring-boot:run

# Terminal 2: Product Service
cd product-catalog/product-service && mvn spring-boot:run

# Terminal 3: Inventory Service
cd product-catalog/inventory-service && mvn spring-boot:run

# Terminal 4: API Gateway
cd product-catalog/api-gateway && mvn spring-boot:run

Bonus Challenges

Challenge 1: Combine Data

Create an endpoint in the gateway that combines product and inventory data. GET /api/products-with-stock should return products with their stock quantities.

Challenge 2: Add Docker

Create a docker-compose.yml that runs all 4 services with a single command.

Challenge 3: Add a Third Service

Create an order-service that creates orders, checking inventory before confirming.