Build a complete microservices ecosystem with service discovery, API gateway, and inter-service communication.
Complete E-Commerce Microservices System:
Architecture:
Client → API Gateway → Product Service
↓ ↓
Eureka Server ← Order Service
Advantages:
Challenges:
Set up Maven multi-module project structure.
ecommerce-microservices/
├── pom.xml (parent)
├── eureka-server/
├── api-gateway/
├── product-service/
├── order-service/
└── docker-compose.yml
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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>ecommerce-microservices</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>
<modules>
<module>eureka-server</module>
<module>api-gateway</module>
<module>product-service</module>
<module>order-service</module>
</modules>
<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>
mkdir ecommerce-microservices
cd ecommerce-microservices
# Create parent POM (as shown above)
# Create module directories
mkdir eureka-server api-gateway product-service order-service
Build service registry for service discovery.
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>ecommerce-microservices</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>eureka-server</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
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
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
src/main/resources/application.yml:
spring:
application:
name: eureka-server
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false # Don't register self
fetch-registry: false # Don't fetch registry
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
server:
wait-time-in-ms-when-sync-empty: 0
cd eureka-server
mvn spring-boot:run
Open http://localhost:8761 - Eureka Dashboard should show no registered services yet.
Build Product Service with Eureka client.
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>ecommerce-microservices</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>product-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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
src/main/java/com/example/product/model/Product.java:
package com.example.product.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
private Long id;
private String name;
private String description;
private BigDecimal price;
private Integer stock;
}
src/main/java/com/example/product/service/ProductService.java:
package com.example.product.service;
import com.example.product.model.Product;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class ProductService {
private final Map<Long, Product> products = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
public ProductService() {
// Initialize with sample data
createProduct(new Product(null, "Laptop", "High-performance laptop",
new BigDecimal("1299.99"), 50));
createProduct(new Product(null, "Mouse", "Wireless mouse",
new BigDecimal("29.99"), 200));
createProduct(new Product(null, "Keyboard", "Mechanical keyboard",
new BigDecimal("89.99"), 100));
}
public List<Product> getAllProducts() {
return new ArrayList<>(products.values());
}
public Optional<Product> getProductById(Long id) {
return Optional.ofNullable(products.get(id));
}
public Product createProduct(Product product) {
product.setId(idGenerator.getAndIncrement());
products.put(product.getId(), product);
return product;
}
public boolean checkStock(Long productId, Integer quantity) {
Product product = products.get(productId);
return product != null && product.getStock() >= quantity;
}
public boolean reduceStock(Long productId, Integer quantity) {
Product product = products.get(productId);
if (product != null && product.getStock() >= quantity) {
product.setStock(product.getStock() - quantity);
return true;
}
return false;
}
}
src/main/java/com/example/product/controller/ProductController.java:
package com.example.product.controller;
import com.example.product.model.Product;
import com.example.product.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private static final Logger log = LoggerFactory.getLogger(ProductController.class);
private final ProductService productService;
@Value("${server.port}")
private String serverPort;
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
log.info("Getting all products from instance on port: {}", serverPort);
return ResponseEntity.ok(productService.getAllProducts());
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
log.info("Getting product {} from instance on port: {}", id, serverPort);
return productService.getProductById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
Product created = productService.createProduct(product);
return ResponseEntity.ok(created);
}
@PostMapping("/{id}/check-stock")
public ResponseEntity<Boolean> checkStock(
@PathVariable Long id,
@RequestParam Integer quantity) {
boolean available = productService.checkStock(id, quantity);
return ResponseEntity.ok(available);
}
@PostMapping("/{id}/reduce-stock")
public ResponseEntity<Boolean> reduceStock(
@PathVariable Long id,
@RequestParam Integer quantity) {
boolean success = productService.reduceStock(id, quantity);
return ResponseEntity.ok(success);
}
}
src/main/java/com/example/product/ProductServiceApplication.java:
package com.example.product;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ProductServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ProductServiceApplication.class, args);
}
}
src/main/resources/application.yml:
spring:
application:
name: product-service
server:
port: 8081
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
management:
endpoints:
web:
exposure:
include: health,info
cd product-service
mvn spring-boot:run
Check Eureka Dashboard - PRODUCT-SERVICE should be registered.
Test API:
curl http://localhost:8081/api/products
Build Order Service that communicates with Product Service via Feign.
order-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>ecommerce-microservices</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>order-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.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
src/main/java/com/example/order/model/Order.java:
package com.example.order.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
private Long id;
private String customerName;
private List<OrderItem> items;
private BigDecimal totalAmount;
private OrderStatus status;
private LocalDateTime createdAt;
}
src/main/java/com/example/order/model/OrderItem.java:
package com.example.order.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderItem {
private Long productId;
private String productName;
private Integer quantity;
}
src/main/java/com/example/order/model/OrderStatus.java:
package com.example.order.model;
public enum OrderStatus {
PENDING, CONFIRMED, CANCELLED
}
src/main/java/com/example/order/client/ProductClient.java:
package com.example.order.client;
import com.example.order.dto.ProductDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(name = "product-service")
public interface ProductClient {
@GetMapping("/api/products/{id}")
ProductDto getProduct(@PathVariable Long id);
@PostMapping("/api/products/{id}/check-stock")
Boolean checkStock(@PathVariable Long id, @RequestParam Integer quantity);
@PostMapping("/api/products/{id}/reduce-stock")
Boolean reduceStock(@PathVariable Long id, @RequestParam Integer quantity);
}
src/main/java/com/example/order/dto/ProductDto.java:
package com.example.order.dto;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ProductDto {
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
}
src/main/java/com/example/order/service/OrderService.java:
package com.example.order.service;
import com.example.order.client.ProductClient;
import com.example.order.dto.ProductDto;
import com.example.order.model.Order;
import com.example.order.model.OrderItem;
import com.example.order.model.OrderStatus;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Service
@RequiredArgsConstructor
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
private final ProductClient productClient;
private final Map<Long, Order> orders = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
public List<Order> getAllOrders() {
return new ArrayList<>(orders.values());
}
public Optional<Order> getOrderById(Long id) {
return Optional.ofNullable(orders.get(id));
}
public Order createOrder(String customerName, List<OrderItem> items) {
log.info("Creating order for customer: {}", customerName);
// Check stock for all items
for (OrderItem item : items) {
log.info("Checking stock for product: {}", item.getProductId());
Boolean stockAvailable = productClient.checkStock(
item.getProductId(),
item.getQuantity()
);
if (!stockAvailable) {
throw new RuntimeException(
"Insufficient stock for product: " + item.getProductId()
);
}
}
// Calculate total and get product names
BigDecimal total = BigDecimal.ZERO;
for (OrderItem item : items) {
ProductDto product = productClient.getProduct(item.getProductId());
item.setProductName(product.getName());
BigDecimal itemTotal = product.getPrice()
.multiply(BigDecimal.valueOf(item.getQuantity()));
total = total.add(itemTotal);
}
// Reduce stock for all items
for (OrderItem item : items) {
log.info("Reducing stock for product: {}", item.getProductId());
productClient.reduceStock(item.getProductId(), item.getQuantity());
}
// Create order
Order order = new Order(
idGenerator.getAndIncrement(),
customerName,
items,
total,
OrderStatus.CONFIRMED,
LocalDateTime.now()
);
orders.put(order.getId(), order);
log.info("Order created: {}", order.getId());
return order;
}
}
src/main/java/com/example/order/controller/OrderController.java:
package com.example.order.controller;
import com.example.order.dto.CreateOrderRequest;
import com.example.order.model.Order;
import com.example.order.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@GetMapping
public ResponseEntity<List<Order>> getAllOrders() {
return ResponseEntity.ok(orderService.getAllOrders());
}
@GetMapping("/{id}")
public ResponseEntity<Order> getOrderById(@PathVariable Long id) {
return orderService.getOrderById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
Order order = orderService.createOrder(
request.getCustomerName(),
request.getItems()
);
return ResponseEntity.ok(order);
}
}
src/main/java/com/example/order/dto/CreateOrderRequest.java:
package com.example.order.dto;
import com.example.order.model.OrderItem;
import lombok.Data;
import java.util.List;
@Data
public class CreateOrderRequest {
private String customerName;
private List<OrderItem> items;
}
src/main/java/com/example/order/OrderServiceApplication.java:
package com.example.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
src/main/resources/application.yml:
spring:
application:
name: order-service
server:
port: 8083
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
management:
endpoints:
web:
exposure:
include: health,info
cd order-service
mvn spring-boot:run
Check Eureka - both services registered.
Create order:
curl -X POST http://localhost:8083/api/orders \
-H "Content-Type: application/json" \
-d '{
"customerName": "John Doe",
"items": [
{"productId": 1, "quantity": 1},
{"productId": 2, "quantity": 2}
]
}'
Build API Gateway as single entry point.
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>ecommerce-microservices</artifactId>
<version>1.0.0</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>
src/main/java/com/example/gateway/ApiGatewayApplication.java:
package com.example.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
src/main/resources/application.yml:
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/**
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
server:
port: 8080
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
management:
endpoints:
web:
exposure:
include: health,info,gateway
logging:
level:
org.springframework.cloud.gateway: DEBUG
cd api-gateway
mvn spring-boot:run
Access via Gateway:
# Products via Gateway
curl http://localhost:8080/api/products
# Orders via Gateway
curl http://localhost:8080/api/orders
# Create order via Gateway
curl -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{
"customerName": "Jane Smith",
"items": [{"productId": 3, "quantity": 1}]
}'
Gateway routes to appropriate service!
Test client-side load balancing with multiple instances.
Terminal 1:
cd product-service
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8081
Terminal 2:
cd product-service
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8082
Check Eureka Dashboard - 2 instances of Product Service.
# Call multiple times
for i in {1..10}; do
curl http://localhost:8080/api/products
echo ""
done
Check logs - requests distributed between ports 8081 and 8082!
Feign uses Spring Cloud LoadBalancer by default.
Customize (optional):
@Configuration
public class LoadBalancerConfig {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withCaching()
.build(context);
}
}
Deploy entire ecosystem with Docker Compose.
eureka-server/Dockerfile:
FROM openjdk:17-slim
COPY target/eureka-server-1.0.0.jar app.jar
EXPOSE 8761
ENTRYPOINT ["java", "-jar", "/app.jar"]
Same pattern for other services (api-gateway/Dockerfile, product-service/Dockerfile, order-service/Dockerfile)
docker-compose.yml (root directory):
version: "3.8"
services:
eureka-server:
build: ./eureka-server
ports:
- "8761:8761"
environment:
- SPRING_PROFILES_ACTIVE=docker
networks:
- microservices-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8761/actuator/health"]
interval: 10s
timeout: 5s
retries: 5
product-service-1:
build: ./product-service
ports:
- "8081:8081"
environment:
- SPRING_PROFILES_ACTIVE=docker
- SERVER_PORT=8081
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka/
depends_on:
eureka-server:
condition: service_healthy
networks:
- microservices-net
product-service-2:
build: ./product-service
ports:
- "8082:8082"
environment:
- SPRING_PROFILES_ACTIVE=docker
- SERVER_PORT=8082
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka/
depends_on:
eureka-server:
condition: service_healthy
networks:
- microservices-net
order-service:
build: ./order-service
ports:
- "8083:8083"
environment:
- SPRING_PROFILES_ACTIVE=docker
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka/
depends_on:
eureka-server:
condition: service_healthy
networks:
- microservices-net
api-gateway:
build: ./api-gateway
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=docker
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka/
depends_on:
eureka-server:
condition: service_healthy
networks:
- microservices-net
networks:
microservices-net:
driver: bridge
# Build all services
mvn clean package -DskipTests
# Start all services
docker-compose up --build
# Check logs
docker-compose logs -f
# Stop all services
docker-compose down
# Via Gateway
curl http://localhost:8080/api/products
curl http://localhost:8080/api/orders
# Eureka UI
open http://localhost:8761
Add health checks and monitoring.
All services have Actuator:
curl http://localhost:8080/actuator/health
curl http://localhost:8081/actuator/health
curl http://localhost:8083/actuator/health
Add Resilience4j for fault tolerance:
pom.xml:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
Feign with Circuit Breaker:
@FeignClient(
name = "product-service",
fallback = ProductClientFallback.class
)
public interface ProductClient {
@GetMapping("/api/products/{id}")
ProductDto getProduct(@PathVariable Long id);
}
@Component
class ProductClientFallback implements ProductClient {
@Override
public ProductDto getProduct(Long id) {
// Return default or cached product
ProductDto fallback = new ProductDto();
fallback.setId(id);
fallback.setName("Product Unavailable");
return fallback;
}
}
Congratulations! 🎉 You've built a complete microservices ecosystem!
Service Registry:
API Gateway:
Service Communication:
Use Microservices When:
Avoid Microservices When: