Microservices with Spring Boot: A Complete Guide
Microservices architecture has revolutionized how we build and deploy applications. Spring Boot, with its ecosystem of tools, makes it easier than ever to create robust, scalable microservices. Let's dive into building a complete microservices system.
Why Microservices with Spring Boot?
Benefits:
- Scalability: Scale individual services based on demand
- Technology Diversity: Use different technologies for different services
- Team Autonomy: Teams can work independently on different services
- Fault Isolation: Failure in one service doesn't bring down the entire system
Setting Up Your First Microservice
Start with a simple Product Service:
<!-- pom.xml -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
<relativePath/>
</parent>
<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.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
</dependencies>
Product Service Implementation
// Product.java
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private BigDecimal price;
@Column(nullable = false)
private Integer quantity;
// Constructors, getters, setters
}
// ProductRepository.java
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByNameContainingIgnoreCase(String name);
List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
}
// ProductService.java
@Service
@Transactional
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List<Product> getAllProducts() {
return productRepository.findAll();
}
public Optional<Product> getProductById(Long id) {
return productRepository.findById(id);
}
public Product createProduct(Product product) {
return productRepository.save(product);
}
public Product updateProduct(Long id, Product productDetails) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException("Product not found with id: " + id));
product.setName(productDetails.getName());
product.setPrice(productDetails.getPrice());
product.setQuantity(productDetails.getQuantity());
return productRepository.save(product);
}
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
}
// ProductController.java
@RestController
@RequestMapping("/api/products")
@Validated
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
List<Product> products = productService.getAllProducts();
return ResponseEntity.ok(products);
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Optional<Product> product = productService.getProductById(id);
return product.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product createdProduct = productService.createProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(createdProduct);
}
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@Valid @RequestBody Product productDetails) {
try {
Product updatedProduct = productService.updateProduct(id, productDetails);
return ResponseEntity.ok(updatedProduct);
} catch (ProductNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
}
Service Discovery with Eureka
Set up Eureka Server:
// EurekaServerApplication.java
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
Configure your microservices as Eureka clients:
# application.yml for Product Service
spring:
application:
name: product-service
datasource:
url: jdbc:h2:mem:productdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
hostname: localhost
server:
port: 8081
Inter-Service Communication
Use OpenFeign for declarative REST client:
// OrderService calling ProductService
@FeignClient(name = "product-service")
public interface ProductServiceClient {
@GetMapping("/api/products/{id}")
Product getProductById(@PathVariable Long id);
@PutMapping("/api/products/{id}")
Product updateProduct(@PathVariable Long id, @RequestBody Product product);
}
// OrderService.java
@Service
public class OrderService {
private final ProductServiceClient productServiceClient;
public OrderService(ProductServiceClient productServiceClient) {
this.productServiceClient = productServiceClient;
}
public Order createOrder(OrderRequest orderRequest) {
// Validate product exists and has sufficient quantity
Product product = productServiceClient.getProductById(orderRequest.getProductId());
if (product == null) {
throw new IllegalArgumentException("Product not found");
}
if (product.getQuantity() < orderRequest.getQuantity()) {
throw new IllegalArgumentException("Insufficient stock");
}
// Update product quantity
product.setQuantity(product.getQuantity() - orderRequest.getQuantity());
productServiceClient.updateProduct(product.getId(), product);
// Create order
Order order = new Order();
order.setProductId(orderRequest.getProductId());
order.setQuantity(orderRequest.getQuantity());
order.setTotalPrice(product.getPrice().multiply(BigDecimal.valueOf(orderRequest.getQuantity())));
return orderRepository.save(order);
}
}
API Gateway with Spring Cloud Gateway
// ApiGatewayApplication.java
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
# application.yml for API Gateway
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/products/**
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
server:
port: 8080
Circuit Breaker with Resilience4j
Add fault tolerance to your microservices:
@Service
public class OrderService {
@CircuitBreaker(name = "product-service", fallbackMethod = "fallbackGetProduct")
@Retry(name = "product-service")
@TimeLimiter(name = "product-service")
public CompletableFuture<Product> getProductById(Long id) {
return CompletableFuture.supplyAsync(() ->
productServiceClient.getProductById(id)
);
}
public CompletableFuture<Product> fallbackGetProduct(Long id, Exception ex) {
// Return cached product or default product
Product defaultProduct = new Product();
defaultProduct.setId(id);
defaultProduct.setName("Product Unavailable");
defaultProduct.setPrice(BigDecimal.ZERO);
defaultProduct.setQuantity(0);
return CompletableFuture.completedFuture(defaultProduct);
}
}
Configuration Management
Use Spring Cloud Config for centralized configuration:
# bootstrap.yml
spring:
application:
name: product-service
cloud:
config:
uri: http://localhost:8888
fail-fast: true
retry:
initial-interval: 1000
max-attempts: 6
max-interval: 2000
multiplier: 1.1
Monitoring and Observability
Add Micrometer for metrics:
// Custom metrics
@Component
public class ProductMetrics {
private final Counter productCreatedCounter;
private final Timer productSearchTimer;
public ProductMetrics(MeterRegistry meterRegistry) {
this.productCreatedCounter = Counter.builder("products.created")
.description("Number of products created")
.register(meterRegistry);
this.productSearchTimer = Timer.builder("products.search.duration")
.description("Product search duration")
.register(meterRegistry);
}
public void incrementProductCreated() {
productCreatedCounter.increment();
}
public Timer.Sample startSearchTimer() {
return Timer.start(productSearchTimer);
}
}
Best Practices
- Database Per Service: Each service should have its own database
- API Versioning: Use proper versioning strategies
- Security: Implement OAuth2/JWT for authentication
- Monitoring: Use distributed tracing with Zipkin/Jaeger
- Testing: Implement contract testing with Pact
- Documentation: Use OpenAPI/Swagger for API documentation
Deployment with Docker
# Dockerfile for microservice
FROM openjdk:17-jre-slim
WORKDIR /app
COPY target/product-service-1.0.0.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]
# docker-compose.yml
version: '3.8'
services:
eureka-server:
build: ./eureka-server
ports:
- "8761:8761"
product-service:
build: ./product-service
ports:
- "8081:8081"
depends_on:
- eureka-server
environment:
- EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://eureka-server:8761/eureka
order-service:
build: ./order-service
ports:
- "8082:8082"
depends_on:
- eureka-server
- product-service
Conclusion
Building microservices with Spring Boot provides a robust, scalable architecture for modern applications. The Spring Cloud ecosystem offers all the tools you need for service discovery, configuration management, circuit breakers, and more.
Ready to dive deeper? Next, I'll cover advanced topics like event-driven architecture and CQRS patterns in microservices!
