Microservices Architecture with Java — Complete Guide
In this tutorial, you will learn about Microservices Architecture with Java. We cover key concepts, practical examples, and best practices to help you master this topic.
From Monolith to Microservices
A monolithic application packages all functionality into a single deployable unit. While simple to develop initially, monoliths become difficult to scale, maintain, and deploy as they grow. Microservices decompose an application into small, independently deployable services, each owning its own data and business domain. Each service can be developed, deployed, and scaled independently by separate teams using potentially different technologies.
Java, with its mature ecosystem of frameworks (Spring Boot, Quarkus, Micronaut), container support (Docker, Kubernetes), and cloud-native libraries, is a leading choice for building microservices. This lesson covers the core patterns and technologies for building microservices with Java.
flowchart TB
Client[Client] --> Gateway[API Gateway]
Gateway --> ServiceA[User Service]
Gateway --> ServiceB[Order Service]
Gateway --> ServiceC[Payment Service]
Gateway --> ServiceD[Inventory Service]
ServiceA --> DB1[(User DB)]
ServiceB --> DB2[(Order DB)]
ServiceC --> DB3[(Payment DB)]
ServiceD --> DB4[(Inventory DB)]
ServiceB -->|gRPC/REST| ServiceC
ServiceB -->|Message Queue| ServiceD
ServiceA -.->|Service Discovery| Registry[Eureka/Consul]
ServiceB -.-> Registry
ServiceC -.-> Registry
Service Discovery with Eureka
Service discovery enables microservices to find each other without hardcoded addresses.
Eureka Server
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServiceApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServiceApplication.class, args);
}
}
# application.yml
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false
Eureka Client
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
spring:
application:
name: user-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
API Gateway with Spring Cloud Gateway
The API gateway is the single entry point for all clients. It handles routing, authentication, rate limiting, and request transformation.
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/v1/users/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter:
replenishRate: 100
burstCapacity: 200
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/v1/orders/**
Inter-Service Communication
Synchronous: REST with Resilience4J
@Service
public class OrderService {
private final RestTemplate restTemplate;
private final InventoryClient inventoryClient;
public OrderService(RestTemplate restTemplate, InventoryClient inventoryClient) {
this.restTemplate = restTemplate;
this.inventoryClient = inventoryClient;
}
@CircuitBreaker(name = "inventory", fallbackMethod = "inventoryFallback")
@Retry(name = "inventory", maxAttempts = 3)
@TimeLimiter(name = "inventory")
public OrderResponse placeOrder(OrderRequest request) {
// Check inventory via Feign client with circuit breaker
InventoryCheckResponse inventory = inventoryClient.checkAvailability(
request.productId(), request.quantity());
if (!inventory.available()) {
throw new OutOfStockException(request.productId());
}
// Process order...
return processOrder(request);
}
public OrderResponse inventoryFallback(OrderRequest request, Throwable t) {
// Fallback: queued for later processing
orderQueue.enqueue(request);
return new OrderResponse("QUEUED", "Order queued for processing");
}
}
Async: Message Queues with RabbitMQ
@Configuration
public class MessagingConfig {
@Bean
public Queue orderQueue() {
return new Queue("order.events", true);
}
@Bean
public Exchange orderExchange() {
return new TopicExchange("order.exchange");
}
@Bean
public Binding binding(Queue queue, Exchange exchange) {
return BindingBuilder.bind(queue)
.to(exchange)
.with("order.*")
.noargs();
}
}
// Producer
@Service
public class OrderEventPublisher {
private final RabbitTemplate rabbitTemplate;
public OrderEventPublisher(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
public void publishOrderCreated(OrderCreatedEvent event) {
rabbitTemplate.convertAndSend("order.exchange", "order.created", event);
}
}
// Consumer
@Component
public class InventoryEventHandler {
@RabbitListener(queues = "order.events")
public void handleOrderCreated(OrderCreatedEvent event) {
// Update inventory asynchronously
inventoryService.reserveStock(event.productId(), event.quantity());
}
}
Configuration Management with Spring Cloud Config
# application.yml on Config Server
spring:
cloud:
config:
server:
git:
uri: https://github.com/company/config-repo
searchPaths: '{application}'
# Config client bootstrap.yml
spring:
application:
name: user-service
cloud:
config:
uri: http://config-server:8888
Containerization with Docker
# Dockerfile for microservice
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/user-service.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
# docker-compose.yml
version: '3.8'
services:
discovery-service:
build: ./discovery-service
ports:
- "8761:8761"
config-server:
build: ./config-server
ports:
- "8888:8888"
user-service:
build: ./user-service
ports:
- "8081:8080"
environment:
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery-service:8761/eureka/
depends_on:
- discovery-service
- config-server
order-service:
build: ./order-service
ports:
- "8082:8080"
environment:
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery-service:8761/eureka/
depends_on:
- discovery-service
- config-server
gateway:
build: ./gateway
ports:
- "8080:8080"
depends_on:
- discovery-service
- user-service
- order-service
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672"
Distributed Tracing with Micrometer and Zipkin
management:
tracing:
sampling:
probability: 1.0
zipkin:
tracing:
endpoint: http://zipkin:9411/api/v2/spans
// Custom tracing
@Bean
public ObservationHandler<Observation.Context> customHandler() {
return new ObservationHandler<>() {
@Override
public void onStart(Observation.Context context) {
System.out.println("Starting: " + context.getName());
}
@Override
public boolean supportsContext(Observation.Context context) {
return true;
}
};
}
Common Mistakes
1. Distributed Monolith
Breaking an application into services but deploying them together with shared databases and tight coupling creates a distributed monolith, which has the worst of both worlds.
2. Shared Database Between Services
Each microservice should own its data. Sharing a database creates coupling and makes independent deployment impossible.
3. Ignoring Network Failures
Networks are unreliable. Always use circuit breakers, retries with backoff, timeouts, and fallbacks.
@CircuitBreaker(name = "payment", fallbackMethod = "paymentFallback")
@Retry(name = "payment", backoff = @Backoff(delay = 1000, multiplier = 2))
4. Overly Fine-Grained Services
Services should map to bounded contexts, not database tables. A service with a single CRUD endpoint per table creates excessive network overhead.
5. Manual Testing Without Contract Tests
Consumer-driven contract tests (Spring Cloud Contract, Pact) ensure that service changes do not break downstream consumers.
6. Missing Observability
Every service must expose health checks, metrics, and distributed tracing. Without observability, debugging microservices failures is extremely difficult.
Practice Questions
- What is the difference between orchestration and choreography in microservices?
- Why should each microservice have its own database?
- How does a circuit breaker improve system resilience?
- What is the role of an API gateway in a microservices architecture?
- How do you handle distributed transactions without two-phase commit?
Challenge: Build a three-service e-commerce system: product-service (inventory), order-service (order management), and notification-service (email/SMS). Implement service discovery with Eureka, inter-service communication via RabbitMQ for order events, and an API gateway with rate limiting. Add distributed tracing and a health dashboard.
FAQ
Mini Project: Event-Driven Microservices Platform
Build a microservices platform for a ticket booking system:
booking-service: Manages ticket reservations (Java, Spring Boot, PostgreSQL)payment-service: Processes payments (Java, Spring Boot, MongoDB)notification-service: Sends confirmations (Java, Spring Boot, Redis)analytics-service: Tracks booking metrics (Python or Java, timeseries DB optional)
Implement:
- Event-driven communication via RabbitMQ (booking.created -> payment.processed -> notification.sent)
- Sagas for rollback if payment fails
- Service discovery with Eureka
- API Gateway with Spring Cloud Gateway
- Circuit breakers with Resilience4J
- Distributed tracing with Micrometer and Zipkin
- Docker Compose for local development
What's Next
You now understand microservices architecture. In the next lesson, you will apply everything you have learned to build a Mini Project 1 - E-Commerce Backend.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro