Skip to main content

Microservices with Spring Boot: A Complete Guide

· 4 min read
Shiaondo Orkuma
AI Engineer & Full Stack Developer @ Hash Dynamics

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

  1. Database Per Service: Each service should have its own database
  2. API Versioning: Use proper versioning strategies
  3. Security: Implement OAuth2/JWT for authentication
  4. Monitoring: Use distributed tracing with Zipkin/Jaeger
  5. Testing: Implement contract testing with Pact
  6. 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!