Microservices architecture involves building applications as a collection of small, independent services that communicate over well-defined APIs. Here's a comprehensive guide to building microservices with Spring Boot.
1. Microservice Project Structure
Typical Microservices Architecture
microservices-project/ ├── service-registry/ # Eureka Server ├── api-gateway/ # Spring Cloud Gateway ├── config-server/ # Spring Cloud Config ├── user-service/ # Microservice 1 ├── order-service/ # Microservice 2 ├── product-service/ # Microservice 3 └── notification-service/ # Microservice 4
2. Service Registry with Eureka
Eureka Server Configuration
// service-registry/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
// service-registry/src/main/java/com/example/ServiceRegistryApplication.java
package com.example.serviceregistry;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class ServiceRegistryApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceRegistryApplication.class, args);
}
}
// service-registry/src/main/resources/application.yml
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 5000
spring:
application:
name: service-registry
3. API Gateway with Spring Cloud Gateway
Gateway Configuration
// api-gateway/pom.xml
<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>
// api-gateway/src/main/java/com/example/ApiGatewayApplication.java
package com.example.apigateway;
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);
}
}
// api-gateway/src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
- name: CircuitBreaker
args:
name: userService
fallbackUri: forward:/fallback/user-service
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/products/**
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
management:
endpoints:
web:
exposure:
include: health,info,gateway
// Custom Gateway Filters
package com.example.apigateway.filters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class LoggingFilter implements GlobalFilter, Ordered {
private final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("Request: {} {}", exchange.getRequest().getMethod(),
exchange.getRequest().getPath());
// Add correlation ID to headers
String correlationId = java.util.UUID.randomUUID().toString();
exchange.getRequest().mutate()
.header("X-Correlation-ID", correlationId)
.build();
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
logger.info("Response status: {}", exchange.getResponse().getStatusCode());
}));
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
// Fallback Controller
package com.example.apigateway.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.Map;
@RestController
public class FallbackController {
@GetMapping("/fallback/user-service")
public ResponseEntity<Map<String, String>> userServiceFallback() {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(Collections.singletonMap("message",
"User service is temporarily unavailable. Please try again later."));
}
}
4. User Service Microservice
Complete User Service Implementation
// user-service/pom.xml
<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>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>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
// user-service/src/main/java/com/example/userservice/UserServiceApplication.java
package com.example.userservice;
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 UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
// Entity
package com.example.userservice.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Name is required")
private String name;
@Email(message = "Email should be valid")
@NotBlank(message = "Email is required")
@Column(unique = true)
private String email;
private String phone;
@Enumerated(EnumType.STRING)
private UserStatus status = UserStatus.ACTIVE;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Constructors, Getters, Setters
public User() {}
public User(String name, String email, String phone) {
this.name = name;
this.email = email;
this.phone = phone;
}
// 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 getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public UserStatus getStatus() { return status; }
public void setStatus(UserStatus status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}
enum UserStatus {
ACTIVE, INACTIVE, SUSPENDED
}
// Repository
package com.example.userservice.repository;
import com.example.userservice.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
}
// DTOs
package com.example.userservice.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public class UserRequest {
@NotBlank(message = "Name is required")
private String name;
@Email(message = "Email should be valid")
@NotBlank(message = "Email is required")
private String email;
private String phone;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
}
package com.example.userservice.dto;
import java.time.LocalDateTime;
public class UserResponse {
private Long id;
private String name;
private String email;
private String phone;
private String status;
private LocalDateTime createdAt;
// Constructors
public UserResponse() {}
public UserResponse(Long id, String name, String email, String phone, String status, LocalDateTime createdAt) {
this.id = id;
this.name = name;
this.email = email;
this.phone = phone;
this.status = status;
this.createdAt = createdAt;
}
// 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 getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
// Service Layer
package com.example.userservice.service;
import com.example.userservice.dto.UserRequest;
import com.example.userservice.dto.UserResponse;
import com.example.userservice.entity.User;
import com.example.userservice.entity.UserStatus;
import com.example.userservice.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
public UserResponse createUser(UserRequest userRequest) {
if (userRepository.existsByEmail(userRequest.getEmail())) {
throw new RuntimeException("User with email " + userRequest.getEmail() + " already exists");
}
User user = new User(userRequest.getName(), userRequest.getEmail(), userRequest.getPhone());
User savedUser = userRepository.save(user);
return mapToUserResponse(savedUser);
}
@Transactional(readOnly = true)
public List<UserResponse> getAllUsers() {
return userRepository.findAll().stream()
.map(this::mapToUserResponse)
.collect(Collectors.toList());
}
@Transactional(readOnly = true)
public UserResponse getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found with id: " + id));
return mapToUserResponse(user);
}
public UserResponse updateUser(Long id, UserRequest userRequest) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found with id: " + id));
user.setName(userRequest.getName());
user.setPhone(userRequest.getPhone());
User updatedUser = userRepository.save(user);
return mapToUserResponse(updatedUser);
}
public void deleteUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found with id: " + id));
user.setStatus(UserStatus.INACTIVE);
userRepository.save(user);
}
private UserResponse mapToUserResponse(User user) {
return new UserResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getPhone(),
user.getStatus().name(),
user.getCreatedAt()
);
}
}
// Controller
package com.example.userservice.controller;
import com.example.userservice.dto.UserRequest;
import com.example.userservice.dto.UserResponse;
import com.example.userservice.service.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody UserRequest userRequest) {
UserResponse userResponse = userService.createUser(userRequest);
return ResponseEntity.status(HttpStatus.CREATED).body(userResponse);
}
@GetMapping
public ResponseEntity<List<UserResponse>> getAllUsers() {
List<UserResponse> users = userService.getAllUsers();
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUserById(@PathVariable Long id) {
UserResponse user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserRequest userRequest) {
UserResponse userResponse = userService.updateUser(id, userRequest);
return ResponseEntity.ok(userResponse);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
// Global Exception Handler
package com.example.userservice.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException ex) {
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.badRequest().body(errorResponse);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
}
class ErrorResponse {
private int status;
private String message;
private LocalDateTime timestamp;
public ErrorResponse(int status, String message, LocalDateTime timestamp) {
this.status = status;
this.message = message;
this.timestamp = timestamp;
}
// Getters and setters
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
}
// user-service/src/main/resources/application.yml
server:
port: 8081
spring:
application:
name: user-service
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
show-sql: true
h2:
console:
enabled: true
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
logging:
level:
com.example.userservice: DEBUG
5. Order Service with Feign Client
Order Service Implementation
// order-service/src/main/java/com/example/orderservice/OrderServiceApplication.java
package com.example.orderservice;
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);
}
}
// Feign Client for User Service
package com.example.orderservice.client;
import com.example.orderservice.dto.UserResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(name = "user-service", fallback = UserServiceFallback.class)
public interface UserServiceClient {
@GetMapping("/api/users/{userId}")
UserResponse getUserById(@PathVariable("userId") Long userId);
}
// Fallback Implementation
package com.example.orderservice.client;
import com.example.orderservice.dto.UserResponse;
import org.springframework.stereotype.Component;
@Component
public class UserServiceFallback implements UserServiceClient {
@Override
public UserResponse getUserById(Long userId) {
// Return a default user when user service is unavailable
return new UserResponse(userId, "Unknown User", "[email protected]",
"N/A", "UNAVAILABLE", null);
}
}
// Order Entity
package com.example.orderservice.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId;
private BigDecimal totalAmount;
@Enumerated(EnumType.STRING)
private OrderStatus status = OrderStatus.PENDING;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<>();
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Helper methods
public void addOrderItem(OrderItem item) {
orderItems.add(item);
item.setOrder(this);
}
// Constructors, Getters, Setters
public Order() {}
public Order(Long userId) {
this.userId = userId;
this.totalAmount = BigDecimal.ZERO;
}
// Getters and setters...
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public BigDecimal getTotalAmount() { return totalAmount; }
public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; }
public OrderStatus getStatus() { return status; }
public void setStatus(OrderStatus status) { this.status = status; }
public List<OrderItem> getOrderItems() { return orderItems; }
public void setOrderItems(List<OrderItem> orderItems) { this.orderItems = orderItems; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}
@Entity
@Table(name = "order_items")
class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long productId;
private Integer quantity;
private BigDecimal price;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
// Constructors, Getters, Setters
public OrderItem() {}
public OrderItem(Long productId, Integer quantity, BigDecimal price) {
this.productId = productId;
this.quantity = quantity;
this.price = price;
}
// Getters and setters...
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getProductId() { return productId; }
public void setProductId(Long productId) { this.productId = productId; }
public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }
public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }
public Order getOrder() { return order; }
public void setOrder(Order order) { this.order = order; }
}
enum OrderStatus {
PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}
// DTOs
package com.example.orderservice.dto;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
public class OrderRequest {
private Long userId;
private List<OrderItemRequest> items;
// Getters and setters
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public List<OrderItemRequest> getItems() { return items; }
public void setItems(List<OrderItemRequest> items) { this.items = items; }
}
class OrderItemRequest {
private Long productId;
private Integer quantity;
private BigDecimal price;
// Getters and setters
public Long getProductId() { return productId; }
public void setProductId(Long productId) { this.productId = productId; }
public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }
public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }
}
public class OrderResponse {
private Long id;
private Long userId;
private UserResponse user;
private BigDecimal totalAmount;
private String status;
private List<OrderItemResponse> items;
private LocalDateTime createdAt;
// Constructors, Getters, Setters
public OrderResponse() {}
// Getters and setters...
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public UserResponse getUser() { return user; }
public void setUser(UserResponse user) { this.user = user; }
public BigDecimal getTotalAmount() { return totalAmount; }
public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public List<OrderItemResponse> getItems() { return items; }
public void setItems(List<OrderItemResponse> items) { this.items = items; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
class OrderItemResponse {
private Long productId;
private Integer quantity;
private BigDecimal price;
// Getters and setters
public Long getProductId() { return productId; }
public void setProductId(Long productId) { this.productId = productId; }
public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }
public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }
}
// Service
package com.example.orderservice.service;
import com.example.orderservice.client.UserServiceClient;
import com.example.orderservice.dto.*;
import com.example.orderservice.entity.Order;
import com.example.orderservice.entity.OrderItem;
import com.example.orderservice.entity.OrderStatus;
import com.example.orderservice.repository.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Transactional
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private UserServiceClient userServiceClient;
public OrderResponse createOrder(OrderRequest orderRequest) {
// Validate user exists (using Feign client)
UserResponse user = userServiceClient.getUserById(orderRequest.getUserId());
Order order = new Order(orderRequest.getUserId());
// Add order items and calculate total
BigDecimal totalAmount = BigDecimal.ZERO;
for (OrderItemRequest itemRequest : orderRequest.getItems()) {
OrderItem orderItem = new OrderItem(
itemRequest.getProductId(),
itemRequest.getQuantity(),
itemRequest.getPrice()
);
order.addOrderItem(orderItem);
totalAmount = totalAmount.add(itemRequest.getPrice().multiply(
BigDecimal.valueOf(itemRequest.getQuantity())));
}
order.setTotalAmount(totalAmount);
Order savedOrder = orderRepository.save(order);
return mapToOrderResponse(savedOrder, user);
}
@Transactional(readOnly = true)
public List<OrderResponse> getAllOrders() {
return orderRepository.findAll().stream()
.map(order -> {
UserResponse user = userServiceClient.getUserById(order.getUserId());
return mapToOrderResponse(order, user);
})
.collect(Collectors.toList());
}
@Transactional(readOnly = true)
public OrderResponse getOrderById(Long id) {
Order order = orderRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Order not found with id: " + id));
UserResponse user = userServiceClient.getUserById(order.getUserId());
return mapToOrderResponse(order, user);
}
public OrderResponse updateOrderStatus(Long id, OrderStatus status) {
Order order = orderRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Order not found with id: " + id));
order.setStatus(status);
Order updatedOrder = orderRepository.save(order);
UserResponse user = userServiceClient.getUserById(updatedOrder.getUserId());
return mapToOrderResponse(updatedOrder, user);
}
private OrderResponse mapToOrderResponse(Order order, UserResponse user) {
OrderResponse response = new OrderResponse();
response.setId(order.getId());
response.setUserId(order.getUserId());
response.setUser(user);
response.setTotalAmount(order.getTotalAmount());
response.setStatus(order.getStatus().name());
response.setCreatedAt(order.getCreatedAt());
response.setItems(order.getOrderItems().stream()
.map(item -> {
OrderItemResponse itemResponse = new OrderItemResponse();
itemResponse.setProductId(item.getProductId());
itemResponse.setQuantity(item.getQuantity());
itemResponse.setPrice(item.getPrice());
return itemResponse;
})
.collect(Collectors.toList()));
return response;
}
}
// Controller
package com.example.orderservice.controller;
import com.example.orderservice.dto.OrderRequest;
import com.example.orderservice.dto.OrderResponse;
import com.example.orderservice.entity.OrderStatus;
import com.example.orderservice.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest orderRequest) {
OrderResponse orderResponse = orderService.createOrder(orderRequest);
return ResponseEntity.status(HttpStatus.CREATED).body(orderResponse);
}
@GetMapping
public ResponseEntity<List<OrderResponse>> getAllOrders() {
List<OrderResponse> orders = orderService.getAllOrders();
return ResponseEntity.ok(orders);
}
@GetMapping("/{id}")
public ResponseEntity<OrderResponse> getOrderById(@PathVariable Long id) {
OrderResponse order = orderService.getOrderById(id);
return ResponseEntity.ok(order);
}
@PatchMapping("/{id}/status")
public ResponseEntity<OrderResponse> updateOrderStatus(
@PathVariable Long id,
@RequestParam OrderStatus status) {
OrderResponse order = orderService.updateOrderStatus(id, status);
return ResponseEntity.ok(order);
}
}
6. Configuration with Spring Cloud Config
Config Server Setup
// config-server/pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
</dependencies>
// config-server/src/main/java/com/example/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
cloud:
config:
server:
git:
uri: https://github.com/your-repo/config-repo
default-label: main
clone-on-start: true
// Client-side configuration (user-service/src/main/resources/bootstrap.yml)
spring:
application:
name: user-service
cloud:
config:
uri: http://localhost:8888
fail-fast: true
7. Distributed Tracing with Sleuth and Zipkin
Adding Distributed Tracing
// Add to each microservice's pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
// application.yml configuration
spring:
sleuth:
sampler:
probability: 1.0 # 100% sampling for development
zipkin:
base-url: http://localhost:9411
// Custom trace interceptor
package com.example.userservice.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class TraceInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(TraceInterceptor.class);
private final Tracer tracer;
public TraceInterceptor(Tracer tracer) {
this.tracer = tracer;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String traceId = tracer.currentSpan().context().traceId();
String spanId = tracer.currentSpan().context().spanId();
logger.info("Incoming request - TraceId: {}, SpanId: {}, Path: {}",
traceId, spanId, request.getRequestURI());
// Add trace ID to response headers
response.addHeader("X-Trace-Id", traceId);
return true;
}
}
8. Resilience Patterns with Resilience4j
Circuit Breaker and Retry Patterns
// Add to pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
// Configuration
package com.example.orderservice.config;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class ResilienceConfig {
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.timeLimiterConfig(TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(5))
.build())
.circuitBreakerConfig(CircuitBreakerConfig.custom()
.slidingWindowSize(10)
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(10))
.build())
.build());
}
}
// Using Circuit Breaker in Service
package com.example.orderservice.service;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
@Autowired
private ProductServiceClient productServiceClient;
@CircuitBreaker(name = "productService", fallbackMethod = "getProductFallback")
@Retry(name = "productService")
public ProductResponse getProduct(Long productId) {
return productServiceClient.getProductById(productId);
}
private ProductResponse getProductFallback(Long productId, Exception ex) {
// Fallback logic
return new ProductResponse(productId, "Fallback Product", BigDecimal.ZERO, "UNAVAILABLE");
}
}
9. Docker Configuration
Dockerfile for Each Microservice
# user-service/Dockerfile FROM openjdk:17-jdk-slim VOLUME /tmp COPY target/user-service-1.0.0.jar app.jar ENTRYPOINT ["java", "-jar", "/app.jar"] EXPOSE 8081
Docker Compose
# docker-compose.yml version: '3.8' services: service-registry: build: ./service-registry ports: - "8761:8761" networks: - microservices-network api-gateway: build: ./api-gateway ports: - "8080:8080" depends_on: - service-registry networks: - microservices-network user-service: build: ./user-service ports: - "8081:8081" depends_on: - service-registry networks: - microservices-network order-service: build: ./order-service ports: - "8082:8082" depends_on: - service-registry networks: - microservices-network networks: microservices-network: driver: bridge
Key Microservices Patterns with Spring Boot
- Service Discovery - Eureka for service registration and discovery
- API Gateway - Single entry point with routing, filtering, and security
- Configuration Management - Centralized configuration with Spring Cloud Config
- Inter-Service Communication - Feign clients for REST calls
- Circuit Breaker - Resilience4j for fault tolerance
- Distributed Tracing - Sleuth and Zipkin for request tracking
- Centralized Logging - ELK stack or similar for log aggregation
- Containerization - Docker for consistent deployment
This architecture provides scalability, resilience, and maintainability for modern cloud-native applications.