Introduction to Microservices
Reading Time: 16 Minutes
Difficulty: Intermediate
Topic Summaryโ
As software systems grow, one giant application becomes hard to manage, scale, and deploy. Microservices is an architectural approach where you break a large application into small, independent services โ each doing one thing well. Spring Cloud is the toolkit that makes building these services in Java practical. In this lesson you'll understand when and how to use microservices.
What You'll Learnโ
- What a monolithic architecture is and its limitations
- What microservices are and how they differ from monoliths
- The benefits and real challenges of microservices
- Service discovery with Eureka
- API Gateway pattern
- Inter-service communication: REST and Feign Client
- Overview of Spring Cloud components
- When microservices make sense (and when they don't)
Prerequisitesโ
- Spring Boot Introduction
- REST API with Spring Boot
- Spring Data JPA (for understanding service structure)
- Basic understanding of networking (ports, HTTP)
Explanationโ
What is a Monolithic Application?โ
A monolith is a single, large application where all features โ user management, orders, payments, notifications, reporting โ are deployed together as one unit.
Monolithic Application
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ [User Module] [Order Module] โ
โ [Payment Module] [Notification Module] โ
โ [Inventory Module] [Reporting Module] โ
โ โ
โ All sharing: same database, same โ
โ codebase, same deployment โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Pros of Monolith:
- Simple to develop initially
- Easy to test end-to-end
- No network calls between components
- Simple deployment (one unit)
Cons as it grows:
- One bug can crash the entire application
- Hard to scale (you must scale everything, even if only one module is slow)
- Slow deployments โ changing one line requires deploying the whole app
- Technology lock-in โ can't use different languages/databases for different parts
- Large teams stepping on each other's code constantly
What are Microservices?โ
Microservices is an architectural style where an application is broken into small, independent services. Each service:
- Does one specific thing (single responsibility)
- Has its own database
- Runs independently (its own process, its own deployment)
- Communicates via APIs (HTTP REST or messaging)
- Can be scaled independently
Microservices Architecture
โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ
โ User โ โ Order โ โ Payment โ
โ Service โ โ Service โ โ Service โ
โ :8081 โ โ :8082 โ โ :8083 โ
โ [UserDB] โ โ [OrderDB] โ โ [PayDB] โ
โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ
โ โ โ
โโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโ
โ
[API Gateway :8080]
โ
[Client]
Monolith vs Microservices Comparisonโ
| Aspect | Monolith | Microservices |
|---|---|---|
| Structure | Single codebase | Multiple services |
| Deployment | Deploy everything | Deploy individually |
| Scaling | Scale the whole app | Scale only what's needed |
| Database | Single shared DB | Each service has its own DB |
| Failure | One failure = full downtime | One failure = partial impact |
| Tech Stack | Same for everything | Different per service |
| Complexity | Simple to start | Complex distributed system |
| Team Size | Works well for small teams | Works well for large teams |
Benefits of Microservicesโ
- Independent Deployment โ Deploy the Order Service without touching User Service
- Independent Scaling โ If Payment Service is slow, scale only it
- Technology Flexibility โ User Service in Java, ML service in Python, Analytics in Node.js
- Fault Isolation โ If Notification Service crashes, users can still place orders
- Team Autonomy โ Each team owns one service completely
- Faster Development โ Small, focused codebases are easier to understand
Challenges of Microservicesโ
- Distributed System Complexity โ Network calls fail; you need retry logic, circuit breakers
- Data Consistency โ Transactions across services are hard (no single database)
- Service Discovery โ How does Order Service find Payment Service's address?
- Monitoring โ Logs are scattered across 20 services; you need centralized logging
- Latency โ Every inter-service call adds network overhead
- Testing โ Integration testing across services is complex
- Operational Overhead โ Managing 20 deployments is much harder than 1
Service Discovery โ Eurekaโ
Problem: In a dynamic environment, services start/stop, and their IP addresses change. How does Service A find Service B?
Solution: Eureka Server (Service Registry)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Eureka Server โ
โ Registry: โ
โ - order-service โ localhost:8082 โ
โ - user-service โ localhost:8081 โ
โ - payment-service โ localhost:8083 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Register โ Ask "where is order-service?"
โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ
โ Order โ โ User โ
โ Service โ โ Service โ
โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ
Each service registers itself with Eureka on startup. When User Service wants to call Order Service, it asks Eureka: "Where is order-service?" Eureka returns the address.
Eureka Server Setup:
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
application.properties for Eureka Server:
server.port=8761
spring.application.name=eureka-server
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
Eureka Client (each microservice):
@SpringBootApplication
@EnableEurekaClient
public class OrderServiceApplication { ... }
# order-service application.properties
server.port=8082
spring.application.name=order-service
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
API Gatewayโ
Problem: The client (mobile app, browser) shouldn't need to know about 10 different services on 10 different ports. You need a single entry point.
Solution: API Gateway
The API Gateway is the front door of your microservices system. It:
- Routes requests to the correct service
- Handles cross-cutting concerns: authentication, rate limiting, logging
- Hides internal service structure from clients
Client โ API Gateway (:8080) โ [route /orders/**] โ Order Service (:8082)
โ [route /users/**] โ User Service (:8081)
โ [route /payments/**]โ Payment Service (:8083)
Spring Cloud Gateway configuration:
# application.yml for API Gateway
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service # lb = load balanced via Eureka
predicates:
- Path=/api/users/**
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
- id: payment-service
uri: lb://payment-service
predicates:
- Path=/api/payments/**
Inter-Service Communicationโ
Option 1: RestTemplate (Traditional)โ
@Service
public class OrderService {
private final RestTemplate restTemplate;
public User getUserById(Long userId) {
// Call User Service directly
return restTemplate.getForObject(
"http://user-service/api/users/" + userId,
User.class
);
}
}
Option 2: OpenFeign (Declarative โ Recommended)โ
Feign lets you call another service as if it were a local Java interface:
// Define the client interface โ Feign generates the implementation
@FeignClient(name = "user-service") // name matches the service name in Eureka
public interface UserServiceClient {
@GetMapping("/api/users/{id}")
User getUserById(@PathVariable Long id);
@PostMapping("/api/users")
User createUser(@RequestBody User user);
}
// Use it in your service
@Service
public class OrderService {
private final UserServiceClient userServiceClient;
public OrderService(UserServiceClient userServiceClient) {
this.userServiceClient = userServiceClient;
}
public Order createOrder(Long userId, OrderRequest request) {
// This looks like a local call but actually makes an HTTP request to user-service
User user = userServiceClient.getUserById(userId);
// ... create order logic
}
}
Enable Feign in your main class:
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication { ... }
Spring Cloud Ecosystem Overviewโ
| Component | Purpose |
|---|---|
| Spring Cloud Eureka | Service discovery and registration |
| Spring Cloud Gateway | API Gateway โ single entry point |
| Spring Cloud OpenFeign | Declarative REST client for inter-service calls |
| Spring Cloud Config | Centralized configuration for all services |
| Spring Cloud Sleuth | Distributed tracing (track requests across services) |
| Spring Cloud CircuitBreaker (Resilience4j) | Prevent cascading failures |
| Spring Cloud LoadBalancer | Client-side load balancing |
When to Use Microservicesโ
Use Microservices when:
- โ You have a large, complex system that needs to scale
- โ Multiple large teams work on the same application
- โ Different parts need different technology stacks
- โ Parts of the system have very different load patterns
- โ You need zero-downtime deployments
Stick with Monolith when:
- โ Small team (< 10 developers)
- โ Early-stage product โ requirements change frequently
- โ Limited DevOps/infrastructure expertise
- โ Simple domain with clear boundaries
- โ Performance-critical app where latency matters
Martin Fowler's rule: Start with a monolith. Move to microservices only when the monolith's pain points are clear and real.
Real-World Analogyโ
Monolith is like a Swiss Army knife โ does everything in one tool. Great for everyday use, but if the blade breaks, the whole knife is useless. Can't upgrade just the scissors.
Microservices are like a professional chef's kitchen โ each tool does one job perfectly (a knife for cutting, a blender for blending, an oven for baking). If the blender breaks, the kitchen still functions. You can upgrade the oven without touching the blender. But managing 20 specialized tools requires more organization!
Eureka is the kitchen phone directory โ when the chef needs the baker, they call the directory to find out where the baker is.
API Gateway is the restaurant front desk โ all guests talk to the front desk, who routes them to the right table/kitchen/bar.
Code Exampleโ
Minimal Two-Service Exampleโ
User Service (port 8081)โ
// UserServiceApplication.java
@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
// UserController.java
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public Map<String, Object> getUser(@PathVariable Long id) {
return Map.of("id", id, "name", "Alice Johnson", "email", "alice@example.com");
}
}
# user-service application.properties
server.port=8081
spring.application.name=user-service
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
Order Service (port 8082) โ calls User Service via Feignโ
// Feign client to call User Service
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/api/users/{id}")
Map<String, Object> getUser(@PathVariable Long id);
}
// OrderController.java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final UserClient userClient;
public OrderController(UserClient userClient) {
this.userClient = userClient;
}
@PostMapping
public Map<String, Object> createOrder(@RequestBody Map<String, Object> request) {
Long userId = Long.valueOf(request.get("userId").toString());
String product = (String) request.get("product");
// Call User Service to get user details
Map<String, Object> user = userClient.getUser(userId);
return Map.of(
"orderId", 101,
"product", product,
"user", user,
"status", "CREATED"
);
}
}
Outputโ
POST http://localhost:8082/api/orders
Body: {"userId": 1, "product": "Laptop"}
Response:
{
"orderId": 101,
"product": "Laptop",
"user": {"id": 1, "name": "Alice Johnson", "email": "alice@example.com"},
"status": "CREATED"
}
Common Mistakesโ
- โ Mistake: Starting with microservices for a small project โ โ Fix: Start with a monolith; extract services only when scaling pain is real
- โ Mistake: Sharing a single database across all microservices โ โ Fix: Each service must own its own database โ sharing creates tight coupling
- โ Mistake: Synchronous calls for everything โ โ Fix: For non-critical, high-volume communication, use async messaging (Kafka, RabbitMQ)
- โ Mistake: No circuit breaker for inter-service calls โ โ Fix: Use Resilience4j to prevent one failing service from cascading to others
Best Practicesโ
- Design each microservice around a business capability (User Service, Order Service)
- Each service should have its own database โ no shared databases
- Use asynchronous messaging (Kafka/RabbitMQ) for non-time-critical communication
- Implement Circuit Breaker (Resilience4j) to handle service failures gracefully
- Centralize logging with ELK Stack (Elasticsearch, Logstash, Kibana)
- Use distributed tracing (Spring Cloud Sleuth + Zipkin) to track requests across services
- Each service should be independently deployable โ avoid tightly coupled releases
Interview Questionsโ
Q: What is the difference between monolithic and microservices architecture?
A: A monolith is a single deployable unit containing all application functionality, sharing one codebase and database. Microservices splits the application into small, independent services, each responsible for one capability, having its own database, and communicating via APIs. Microservices offer better scalability and fault isolation but introduce distributed system complexity.
Q: What is service discovery and why is it needed in microservices?
A: In microservices, services run on dynamic IP addresses and ports that can change. Service discovery solves this: services register themselves with a registry (like Eureka) on startup. When Service A wants to call Service B, it asks the registry for Service B's current address instead of hardcoding it. This enables dynamic scaling and deployment.
Q: What is an API Gateway?
A: An API Gateway is a single entry point for all client requests to a microservices system. It routes requests to the appropriate service, handles cross-cutting concerns like authentication, rate limiting, logging, and SSL termination. Clients talk to one URL instead of knowing about 10 different services. Spring Cloud Gateway is the modern implementation.
Q: What is the difference between RestTemplate and Feign Client?
A: RestTemplate is the traditional imperative HTTP client where you manually build URLs and parse responses. OpenFeign is declarative โ you define an interface with annotations and Spring generates the HTTP client implementation. Feign integrates with Eureka for service discovery and is much less verbose. Feign is the recommended approach in modern Spring Cloud applications.
Q: What is a Circuit Breaker in microservices?
A: A Circuit Breaker is a pattern that prevents cascading failures. When Service A calls Service B and B is failing, without a circuit breaker, A will keep getting errors and may also fail. With a circuit breaker (like Resilience4j), after a threshold of failures, the circuit "opens" โ A immediately returns a fallback response without calling B, giving B time to recover.
Quick Revisionโ
โ Monolith = one big app; Microservices = many small independent services
โ Each microservice has its own database, codebase, and deployment
โ Eureka = service registry; services register themselves and discover others
โ API Gateway = single entry point; routes requests to correct service
โ Feign Client = declarative REST client for inter-service calls
โ Spring Cloud provides: Eureka, Gateway, Feign, Config, Sleuth, CircuitBreaker
โ Start with monolith; move to microservices when the pain is real
Related Topicsโ
- Spring Boot REST APIs
- Spring Security
- Docker and Kubernetes (for deploying microservices)
- Message Queues (Kafka, RabbitMQ) for async communication
Next Lessonโ
JDBC โ Lesson 1: Java Database Connectivity