Java Microservices Architecture: Spring Boot, Docker & Kubernetes Guide
Microservices architecture breaks a monolithic application into small, independently deployable services, each owning a specific business capability. In the Java ecosystem, Spring Boot is the dominant framework for building microservices, with Docker and Kubernetes providing the containerisation and orchestration layer.
This guide covers microservices concepts, how to build them with Spring Boot, how to handle inter-service communication, and the key challenges you will face in production.
Microservices vs Monolith
A monolith packages all application functionality into a single deployable unit. This is simple to develop and deploy initially, but as the system grows:
- One change requires deploying the entire application
- A single slow module degrades the whole system
- Technology choices are locked in for the whole codebase
- Teams step on each other's changes
Microservices address these by splitting along business domain lines. An e-commerce platform might have: order-service, inventory-service, payment-service, notification-service — each deployed, scaled, and updated independently.
Building a Microservice with Spring Boot
Each microservice is a standalone Spring Boot application. Typical dependencies:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Service discovery -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
A minimal order service:
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody @Valid CreateOrderRequest req) {
Order order = orderService.create(req);
return ResponseEntity.status(201).body(order);
}
@GetMapping("/{id}")
public Order getOrder(@PathVariable UUID id) {
return orderService.findById(id);
}
}
Service Discovery with Eureka
In a microservices environment, services start and stop dynamically. Service discovery lets services find each other without hardcoded URLs.
Set up a Eureka server:
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServerApplication.class, args);
}
}
Each service registers itself by adding to application.yml:
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: http://discovery-server:8761/eureka/
Inter-Service Communication
Services communicate in two ways:
Synchronous (REST / gRPC)
Use Spring's WebClient for non-blocking HTTP calls between services:
@Service
public class InventoryClient {
private final WebClient webClient;
public InventoryClient(WebClient.Builder builder) {
this.webClient = builder.baseUrl("http://inventory-service").build();
}
public boolean checkStock(String sku, int quantity) {
return Boolean.TRUE.equals(
webClient.get()
.uri("/inventory/{sku}/available?qty={qty}", sku, quantity)
.retrieve()
.bodyToMono(Boolean.class)
.block()
);
}
}
Asynchronous (Message Queues)
For operations where you do not need an immediate response, use Kafka or RabbitMQ. The order service publishes an OrderCreatedEvent; the notification service consumes it and sends an email — fully decoupled.
// Publisher (order-service)
@Service
public class OrderEventPublisher {
private final KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate;
public void publish(OrderCreatedEvent event) {
kafkaTemplate.send("order-events", event.orderId().toString(), event);
}
}
// Consumer (notification-service)
@KafkaListener(topics = "order-events", groupId = "notification-group")
public void onOrderCreated(OrderCreatedEvent event) {
emailService.sendOrderConfirmation(event.customerId(), event.orderId());
}
API Gateway Pattern
Clients should not call individual microservices directly — they should talk to an API gateway. The gateway handles routing, authentication, rate limiting, and SSL termination in one place.
Spring Cloud Gateway setup:
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
- id: inventory-service
uri: lb://inventory-service
predicates:
- Path=/api/inventory/**
The lb:// prefix tells the gateway to use the Eureka load balancer to resolve the service address.
Containerising with Docker
Each microservice gets its own Dockerfile:
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/order-service-1.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Use a multi-stage build to keep the image small and not ship build tools:
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /build/target/order-service-*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
Deploying to Kubernetes
A Kubernetes Deployment and Service for the order microservice:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: techoral/order-service:1.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
---
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- port: 80
targetPort: 8080
Key Challenges in Production
- Distributed tracing: A single user request may touch 5 services. Use Spring Cloud Sleuth + Zipkin or OpenTelemetry to trace calls across services.
- Cascading failures: If inventory-service is slow, calls from order-service pile up and bring it down too. Use the Circuit Breaker pattern (Resilience4j) to fail fast.
- Data consistency: Each service owns its database. Cross-service transactions require the Saga pattern — a sequence of local transactions with compensating rollbacks.
- Configuration management: Use Spring Cloud Config Server or Kubernetes ConfigMaps to manage configuration centrally across all services.
Conclusion
Java microservices with Spring Boot, Docker, and Kubernetes give you independent deployability, scalability per service, and clear ownership boundaries. The cost is operational complexity — service discovery, distributed tracing, and data consistency all require deliberate solutions. Start simple, break services out only when you hit real pain points, and invest in observability from day one.
Related: CQRS Pattern in Java | Java Design Patterns