Skip to content

Microservices Architecture with Java — Complete Guide

DodaTech Updated 2026-06-28 6 min read

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

  1. What is the difference between orchestration and choreography in microservices?
  2. Why should each microservice have its own database?
  3. How does a circuit breaker improve system resilience?
  4. What is the role of an API gateway in a microservices architecture?
  5. 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

What is the difference between a monolith and a microservice?

A monolith is a single deployable unit containing all functionality. Microservices are independently deployable services, each with its own domain and data, communicating over the network.

How do you handle distributed transactions?

Use the Saga pattern, which breaks a distributed transaction into a series of local transactions with compensating actions for rollback. Orchestrated sagas use a coordinator; choreographed sagas use event-driven communication.

Should microservices share code?

Share interfaces and DTOs via shared libraries, but avoid sharing business logic. Each service should own its implementation. Version shared libraries carefully.

What is the difference between Kubernetes and Docker Compose?

Docker Compose is for local development and single-host deployments. Kubernetes is a production-grade container orchestration platform for multi-host, auto-scaling, and self-healing deployments.

Can microservices use different programming languages?

Yes. Microservices can use different languages and frameworks. Polyglot persistence (different databases per service) is also common. However, using the same technology stack reduces operational complexity.

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