Microservices in Spring Boot: Architecture, Design Patterns, and Production Practices
Introduction
Microservices architecture has become the de facto standard for building scalable, resilient, and maintainable applications. In this comprehensive guide, we'll explore how to leverage the power of Spring Boot and Spring Cloud to build a robust microservices ecosystem.
Monolith vs. Microservices
| Feature | Monolithic Architecture | Microservices Architecture |
|---|---|---|
| Deployment | Single unit | Independent services |
| Scaling | Scale everything | Scale individual components |
| Complexity | Low initially, high over time | High initially, managed over time |
| Tech Stack | Single stack | Polyglot (can use best tool for job) |
[!TIP] When to adopt Microservices? Don't start with microservices. Start with a modular monolith. Move to microservices only when specific domains need independent scaling or team velocity is hindered by the monolith.
High-Level Architecture Overview
Before diving into each component, here's how all the pieces fit together in a typical Spring Boot microservices ecosystem:
Core Microservices Components
A robust microservices architecture relies on several key components working in harmony.
1. Service Discovery (Eureka)
In a dynamic environment where service instances come and go, hardcoding IP addresses is impossible. Spring Cloud Netflix Eureka acts as a phone book — services register themselves on startup, and other services look them up by name instead of address.
How Eureka Works
Eureka follows a client-server model:
- Eureka Server: A standalone registry that maintains a list of all available service instances.
- Eureka Clients: Each microservice registers itself with the server on startup and sends periodic heartbeats (every 30 seconds by default) to signal it's still alive.
- Discovery: When Service A needs to call Service B, it asks the Eureka Server for the current list of healthy Service B instances.
Setting Up the Eureka Server
First, add the dependency to your pom.xml:
1// pom.xml (Eureka Server)
2<dependency>
3 <groupId>org.springframework.cloud</groupId>
4 <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
5</dependency>Then, enable the server with a single annotation:
1@SpringBootApplication
2@EnableEurekaServer
3public class ServiceRegistryApplication {
4 public static void main(String[] args) {
5 SpringApplication.run(ServiceRegistryApplication.class, args);
6 }
7}Configure application.yml for the Eureka Server:
1// application.yml (Eureka Server)
2server:
3 port: 8761
4
5eureka:
6 client:
7 register-with-eureka: false # Server doesn't register with itself
8 fetch-registry: false # Server doesn't need to fetch its own registry
9 server:
10 enable-self-preservation: true
11 eviction-interval-timer-in-ms: 5000[!NOTE] Self-Preservation Mode When Eureka detects that too many clients have stopped sending heartbeats (e.g., due to a network partition), it enters self-preservation mode and stops evicting instances. This prevents cascade failures from wiping the registry during temporary network issues.
Registering a Microservice as an Eureka Client
Each microservice needs the client dependency:
1// pom.xml (Eureka Client)
2<dependency>
3 <groupId>org.springframework.cloud</groupId>
4 <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
5</dependency>And the configuration:
1// application.yml (e.g., Order Service)
2spring:
3 application:
4 name: order-service # This is the name other services use to discover it
5
6eureka:
7 client:
8 service-url:
9 defaultZone: http://localhost:8761/eureka/
10 instance:
11 prefer-ip-address: true
12 instance-id: ${spring.application.name}:${random.value}Service-to-Service Communication via Eureka
Once registered, services can call each other by name instead of hardcoded URLs. Use a load-balanced RestTemplate or WebClient:
1@Bean
2@LoadBalanced // Enables service name resolution via Eureka
3public RestTemplate restTemplate() {
4 return new RestTemplate();
5}
6
7// Usage — "inventory-service" is the spring.application.name
8String url = "http://inventory-service/api/v1/stock/{productId}";
9StockResponse stock = restTemplate.getForObject(url, StockResponse.class, productId);The @LoadBalanced annotation integrates with Spring Cloud LoadBalancer, which fetches the list of instances from Eureka and distributes requests using round-robin by default.
Eureka Dashboard
Once running, the Eureka Server provides a built-in dashboard at http://localhost:8761. It shows:
- All registered services and their instances
- Instance health status (UP, DOWN, OUT_OF_SERVICE)
- Self-preservation mode status
- Lease expiration and renewal information
| Eureka Config Property | Default | Purpose |
|---|---|---|
lease-renewal-interval-in-seconds | 30 | How often the client sends heartbeats |
lease-expiration-duration-in-seconds | 90 | How long the server waits before evicting |
registry-fetch-interval-seconds | 30 | How often clients refresh their local cache |
enable-self-preservation | true | Prevents mass eviction during network partitions |
[!TIP] Production Best Practice Run at least two Eureka Server instances in a peer-aware setup for high availability. Configure them to register with each other so the registry survives a single-node failure.
2. API Gateway (Spring Cloud Gateway)
The Gateway is the single entry point for all client requests. It handles routing, security, and rate limiting.
Key responsibilities:
- Routing: Forwarding requests to the correct service.
- Security: Authentication and authorization (OAuth2).
- Resilience: Retry logic and circuit breaking.
3. Inter-service Communication
Services often need to talk to each other.
- Synchronous: REST (using
RestClientorWebClient) or Feign Client. - Asynchronous: Event-driven architecture using Kafka or RabbitMQ.
OpenFeign — Declarative REST Client
While RestTemplate and WebClient work, they require boilerplate code for every service call. Spring Cloud OpenFeign takes a different approach — you declare an interface, and Feign generates the HTTP client for you. Combined with Eureka, it automatically resolves service names and load-balances requests.
Add the dependency:
1// pom.xml
2<dependency>
3 <groupId>org.springframework.cloud</groupId>
4 <artifactId>spring-cloud-starter-openfeign</artifactId>
5</dependency>Enable Feign in your main application:
1@SpringBootApplication
2@EnableFeignClients // Scans for @FeignClient interfaces
3public class OrderServiceApplication {
4 public static void main(String[] args) {
5 SpringApplication.run(OrderServiceApplication.class, args);
6 }
7}Defining a Feign Client
Create an interface that mirrors the target service's API. Feign handles the rest:
1@FeignClient(name = "inventory-service") // Matches the spring.application.name in Eureka
2public interface InventoryClient {
3
4 @GetMapping("/api/v1/stock/{productId}")
5 StockResponse getStock(@PathVariable("productId") String productId);
6
7 @PostMapping("/api/v1/stock/reserve")
8 ReservationResponse reserveStock(@RequestBody ReserveRequest request);
9
10 @GetMapping("/api/v1/stock/bulk")
11 List<StockResponse> getBulkStock(@RequestParam("ids") List<String> productIds);
12}Usage in a service — just inject and call:
1@Service
2@RequiredArgsConstructor
3public class OrderService {
4
5 private final InventoryClient inventoryClient;
6
7 public OrderResponse createOrder(OrderRequest request) {
8 // Feign handles: Eureka lookup → load balancing → HTTP call → deserialization
9 StockResponse stock = inventoryClient.getStock(request.getProductId());
10
11 if (!stock.isAvailable()) {
12 throw new InsufficientStockException("Product out of stock");
13 }
14
15 inventoryClient.reserveStock(new ReserveRequest(request.getProductId(), request.getQuantity()));
16 return orderRepository.save(Order.from(request)).toResponse();
17 }
18}[!TIP] Why Feign over RestTemplate?
- No boilerplate — no URL construction, no
getForObject()calls, no manual deserialization.- Type-safe — compile-time checking of request/response types.
- Built-in integration — works seamlessly with Eureka, LoadBalancer, and Circuit Breakers.
- Readable — the interface reads like API documentation.
Feign with Fallbacks (Resilience)
Combine Feign with Resilience4j to handle failures gracefully:
1@FeignClient(
2 name = "inventory-service",
3 fallback = InventoryFallback.class
4)
5public interface InventoryClient {
6 @GetMapping("/api/v1/stock/{productId}")
7 StockResponse getStock(@PathVariable("productId") String productId);
8}
9
10@Component
11public class InventoryFallback implements InventoryClient {
12 @Override
13 public StockResponse getStock(String productId) {
14 // Return a safe default when inventory-service is down
15 return StockResponse.builder()
16 .productId(productId)
17 .available(false)
18 .message("Inventory service temporarily unavailable")
19 .build();
20 }
21}Feign Configuration
Customize timeouts, logging, and retry behavior per client:
1// application.yml
2spring:
3 cloud:
4 openfeign:
5 client:
6 config:
7 default: # Applies to all Feign clients
8 connect-timeout: 5000
9 read-timeout: 5000
10 logger-level: basic
11 inventory-service: # Override for a specific client
12 connect-timeout: 3000
13 read-timeout: 10000
14 logger-level: full # Logs headers, body, and metadata| Feign Logger Level | What It Logs |
|---|---|
| NONE | No logging (default) |
| BASIC | Request method, URL, response status, and execution time |
| HEADERS | Basic + request/response headers |
| FULL | Headers + request/response body (use only in development) |
Feign with Request Interceptors
Pass authentication tokens or custom headers automatically to all downstream calls:
1@Bean
2public RequestInterceptor authInterceptor() {
3 return template -> {
4 // Forward the JWT token from the current request context
5 String token = SecurityContextHolder.getContext()
6 .getAuthentication().getCredentials().toString();
7 template.header("Authorization", "Bearer " + token);
8 };
9}[!WARNING] Feign is Synchronous Feign clients are blocking by default. For high-throughput scenarios where you don't need the response immediately, prefer asynchronous communication via message queues (Kafka, RabbitMQ). Use Feign for request-reply patterns where you need the result right away.
[!WARNING] Avoid Distributed Transactions Trying to maintain ACID properties across services is a recipe for disaster. Use Sagas or Eventual Consistency patterns instead.
Resilience & Observability
Distributed systems fail. It's not a matter of if, but when.
Circuit Breaker (Resilience4j)
Prevent cascading failures by failing fast when a dependent service is down.
- CLOSED: All requests pass through normally. Failures are counted.
- OPEN: All requests are immediately rejected and routed to the fallback. No calls hit the failing service.
- HALF-OPEN: After a wait period, a few test requests are allowed through. If they succeed, the breaker closes. If they fail, it opens again.
1@CircuitBreaker(name = "inventory", fallbackMethod = "fallbackInventory")
2public String getInventoryStatus(String productId) {
3 return inventoryClient.getStatus(productId);
4}
5
6public String fallbackInventory(String productId, Throwable t) {
7 return "Inventory Temporarily Unavailable";
8}Distributed Tracing
With requests jumping between multiple services, debugging is hard. Tools like Zipkin or Jaeger (via Micrometer Tracing) are essential to visualize the request path.
Each span represents a unit of work. The trace connects all spans for a single request, making it easy to pinpoint exactly where latency or failures occur.
Deployment & Scalability
Docker & Kubernetes
Containerization is non-negotiable. Each Spring Boot application should be packaged as a Docker container.
1FROM eclipse-temurin:17-jdk-alpine
2VOLUME /tmp
3COPY target/*.jar app.jar
4ENTRYPOINT ["java","-jar","/app.jar"]Kubernetes (K8s) orchestrates these containers, handling:
- Auto-scaling: Based on CPU/Memory usage.
- Self-healing: Restarting crashed containers.
- Load Balancing: Distributing traffic.
Conclusion
Migrating to microservices is a journey, not a destination. It requires a shift in mindset—from "building an app" to "building a distributed system."
Key Takeaways:
- Isolate domains carefully (Domain-Driven Design).
- Automate everything (CI/CD, Infrastructure as Code).
- Monitor aggressively (Logs, Metrics, Traces).
Start small, extract one service at a time, and validate your assumptions.