Java Academy

Master Java Development

Menu

Core Java

Servlets

JDBC

Microservices

Build Tools

APIs

Microservices · Lesson 1

Introduction to Microservices

What Are Microservices?

Microservices is an architectural style where a large application is decomposed into small, independent, loosely-coupled services. Each service runs in its own process, handles a specific business capability, and communicates with other services via well-defined APIs (usually HTTP/REST). Instead of one monolithic application, you have multiple services that can be developed, deployed, and scaled independently. This approach enables faster development, easier maintenance, and flexible scaling — but adds complexity in deployment and communication.

Monolithic vs Microservices Architecture

In a monolithic architecture, all features live in a single codebase and run as one process: user management, payments, inventory, notifications. Scaling requires replicating the entire application. Changes to one feature risk breaking others. In microservices, each concern is a separate service: User Service, Payment Service, Inventory Service. Each scales independently. Teams develop in parallel without coordination. Failures are isolated — if the notification service goes down, users can still place orders.

// Monolithic approach (everything in one application)
public class MonolithicApplication {
    public void registerUser(String email) { /* ... */ }
    public void processPayment(int userId, double amount) { /* ... */ }
    public void updateInventory(String productId, int quantity) { /* ... */ }
    public void sendNotification(int userId, String message) { /* ... */ }
}

// Microservices approach (separate, independent services)
// UserService (port 8001)
public class UserService {
    public void registerUser(String email) { /* ... */ }
}

// PaymentService (port 8002)
public class PaymentService {
    public void processPayment(int userId, double amount) { /* ... */ }
}

// InventoryService (port 8003)
public class InventoryService {
    public void updateInventory(String productId, int quantity) { /* ... */ }
}

// NotificationService (port 8004)
public class NotificationService {
    public void sendNotification(int userId, String message) { /* ... */ }
}

Service Communication: Synchronous and Asynchronous

Microservices communicate via synchronous (request-response, immediate) or asynchronous (message-based, decoupled) patterns. Synchronous: Service A calls Service B's REST API, waits for a response. Simple but creates tight coupling — if B is slow, A waits. Asynchronous: Service A publishes an event to a message broker; Service B subscribes and processes it later. Loose coupling, better resilience, but more complex.

// Synchronous communication (REST HTTP calls)
public class OrderService {
    private RestTemplate restTemplate;
    
    public void placeOrder(int userId, String productId, int quantity) {
        // Call InventoryService synchronously
        boolean available = restTemplate.getForObject(
            "http://inventory-service/check?product=" + productId,
            Boolean.class
        );
        
        if (available) {
            // Call PaymentService synchronously
            boolean paid = restTemplate.postForObject(
                "http://payment-service/pay",
                new PaymentRequest(userId, 99.99),
                Boolean.class
            );
            
            if (paid) {
                // Create order
                System.out.println("Order placed successfully");
            }
        }
    }
}

// Asynchronous communication (message queue)
public class OrderServiceAsync {
    private JmsTemplate jmsTemplate;
    
    public void placeOrder(int userId, String productId) {
        // Publish event to message queue
        Order order = new Order(userId, productId);
        jmsTemplate.convertAndSend("order.queue", order);
        System.out.println("Order message published");
        // Don't wait for response
    }
}

// InventoryService listens for order events
@JmsListener(destination = "order.queue")
public void handleOrderEvent(Order order) {
    // Process order asynchronously
    System.out.println("Processing order: " + order.getId());
    updateInventory(order.getProductId());
}

Service Discovery: Finding Services Dynamically

In a microservices environment, services scale up/down dynamically, and instances move around. Service discovery automatically locates available service instances. Client-side discovery: the client queries a registry to find service instances. Server-side discovery: a load balancer queries the registry. Tools like Eureka, Consul, and Kubernetes handle service discovery automatically.

// Manual service location (hardcoded, not scalable)
String paymentServiceUrl = "http://payment-service-1.example.com:8002";

// Service discovery with Eureka (Spring Cloud)
@SpringBootApplication
@EnableDiscoveryClient
public class OrderService {
    @Autowired
    private DiscoveryClient discoveryClient;
    
    public void callPaymentService() {
        // Dynamically discover service instances
        List<ServiceInstance> instances = discoveryClient.getInstances("PAYMENT-SERVICE");
        
        if (!instances.isEmpty()) {
            ServiceInstance instance = instances.get(0);
            String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/pay";
            System.out.println("Calling payment service at: " + url);
        }
    }
}

Deployment: Containers and Orchestration

Microservices are typically deployed in containers (Docker) for consistency and isolation. In production, orchestration tools (Kubernetes) manage containers: scale services based on load, restart failed containers, update services with zero downtime. A Kubernetes cluster runs dozens of microservice instances automatically.

// A simple microservice that runs in a container
@SpringBootApplication
@RestController
public class UserService {
    
    @GetMapping("/health")
    public String health() {
        return "OK";  // Kubernetes liveness probe
    }
    
    @PostMapping("/users")
    public User createUser(@RequestBody UserRequest req) {
        // Business logic
        return new User(req.getName(), req.getEmail());
    }
    
    public static void main(String[] args) {
        SpringApplication.run(UserService.class, args);
    }
}

// Dockerfile for containerization
/*
FROM openjdk:11
COPY target/user-service.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
*/

// Kubernetes deployment manifest
/*
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: myregistry/user-service:latest
        ports:
        - containerPort: 8080
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
*/
AspectMonolithicMicroservices
DeploymentSingle artifactMultiple deployments
ScalingScale entire appScale individual services
TechnologyShared stackDifferent stacks per service
Failure IsolationOne failure crashes allFailures isolated
Development SpeedSlower (large codebase)Faster (small services)
Operational ComplexitySimpleComplex (many moving parts)

Common Mistake: Jumping to microservices too early. Microservices add significant operational complexity (deployment, monitoring, debugging). Start with a monolith; split into microservices when you have scaling or organizational needs. Also, don't share databases between services — each service owns its data, communicating through APIs. Shared databases create tight coupling and data consistency issues.

Best Practices for Microservices

  • Design services by business capability (bounded contexts), not technical layers
  • Each service owns its data — don't share databases
  • Use APIs for all communication — no direct database access
  • Implement circuit breakers to prevent cascading failures
  • Use centralized logging (ELK, Splunk) to track requests across services
  • Monitor services independently — track latency, error rates, resource usage
  • Version your APIs to support gradual updates
  • Plan for eventual consistency — data won't be instantly consistent
  • Start with a monolith if you're unsure; refactor to microservices later

Real-World Example: E-Commerce Platform

An e-commerce platform uses microservices: User Service (authentication, profiles), Product Service (catalog), Order Service (order management), Payment Service (payment processing), Inventory Service (stock management), Notification Service (emails, SMS). Each runs in Docker containers managed by Kubernetes, scaling independently based on demand. On checkout, Order Service calls Product and Inventory Services synchronously to check availability, publishes order events asynchronously for Payment and Notification Services to consume. If Payment Service is overloaded, it returns immediately; Order Service retries with exponential backoff. A service mesh (Istio) provides observability and traffic management. By decomposing the monolith into services, the team can deploy and scale features independently.

Quiz Check

Quick Knowledge Test

Loading quiz questions...