Spring Boot
Introduction to Microservices
Microservices is an architectural style that structures an application as a collection of small, independently deployable services, each running in its own process and communicating over lightweight protocols such as HTTP/REST or messaging queues. Each service is built around a specific business capability, owned by a small team, and can be developed, deployed, and scaled independently.
What Are Microservices?
Microservices decompose a large application into loosely coupled, fine-grained services. Each service focuses on a single business domain (e.g. Users, Orders, Payments), has its own database, and exposes a well-defined API. This contrasts with a monolith, where all functionality is deployed as one unit. The term was popularised by Martin Fowler and James Lewis around 2014, though the underlying ideas (SOA, Unix philosophy) are older.
Java
// ββ Monolith vs Microservices (conceptual structure) βββββββββββββββββ
// MONOLITH β single deployable unit:
//
// ββββββββββββββββββββββββββββββββββββββββ
// β E-Commerce App β
// β ββββββββββ ββββββββββ ββββββββββββ β
// β β User β β Order β β Payment β β
// β β Module β β Module β β Module β β
// β ββββββββββ ββββββββββ ββββββββββββ β
// β Shared Database β
// ββββββββββββββββββββββββββββββββββββββββ
// Deploy everything together.
// One bug can bring down the entire app.
// MICROSERVICES β independent deployable services:
//
// βββββββββββββββ βββββββββββββββ βββββββββββββββ
// β User Serviceβ βOrder Serviceβ βPay Service β
// β :8081 β β :8082 β β :8083 β
// β own DB β β own DB β β own DB β
// ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββ¬βββββββ
// βββββββββββββββββββ΄ββββββββββββββββββ
// communicate via
// REST / gRPC / Events
//
// Deploy, scale, and update each service independently.Core Principles
Microservices follow a set of guiding principles that distinguish them from traditional service-oriented architecture (SOA). Understanding these principles is essential before adopting the pattern, because violating them (e.g. sharing a database across services) produces the worst of both worlds: distributed complexity with monolithic coupling.
Java
// ββ Core Microservices Principles ββββββββββββββββββββββββββββββββββββ
// 1. SINGLE RESPONSIBILITY
// Each service owns exactly one business capability.
// UserService β registration, authentication, profile
// OrderService β cart, checkout, order history
// PayService β billing, invoicing, refunds
// 2. DECENTRALISED DATA MANAGEMENT
// Each service has its own database (DB-per-service pattern).
// Services NEVER share tables or schemas directly.
//
// UserService β PostgreSQL (users table)
// OrderService β MySQL (orders table)
// PayService β MongoDB (transactions collection)
//
// Cross-service data needs β API call or event, never a JOIN.
// 3. INDEPENDENT DEPLOYABILITY
// A team can build, test, and deploy their service
// without coordinating with other teams.
// Requires: stable API contracts + backward compatibility.
// 4. DESIGNED FOR FAILURE
// Any service can fail at any time.
// Callers must handle timeouts, retries, and fallbacks.
// Pattern: Circuit Breaker (Resilience4j / Hystrix).
// 5. DECENTRALISED GOVERNANCE
// Teams choose the right tool for their service:
// UserService β Java / Spring Boot
// RecommendService β Python / FastAPI
// NotifyService β Node.js / ExpressService Communication
Microservices communicate in two primary ways: synchronous (request/response) and asynchronous (event-driven). Synchronous communication via REST or gRPC is simple and familiar but creates temporal coupling β the caller blocks until the callee responds. Asynchronous messaging via a broker (Kafka, RabbitMQ) decouples services in time but introduces eventual consistency.
Java
// ββ Synchronous: REST (HTTP) ββββββββββββββββββββββββββββββββββββββββββ
// OrderService calls UserService to fetch user details:
@Service
@RequiredArgsConstructor
public class OrderService {
private final RestTemplate restTemplate;
// or: WebClient (non-blocking), OpenFeign (declarative)
public OrderResponse placeOrder(CreateOrderRequest request) {
// Synchronous HTTP call to UserService:
UserResponse user = restTemplate.getForObject(
"http://user-service/api/users/" + request.getUserId(),
UserResponse.class
);
// If UserService is down β this call throws β order fails.
// Solution: circuit breaker + fallback (see Resilience4j section).
Order order = Order.builder()
.userId(user.getId())
.items(request.getItems())
.build();
return OrderResponse.from(orderRepository.save(order));
}
}
// ββ Asynchronous: Event-driven (Kafka) βββββββββββββββββββββββββββββββ
// OrderService publishes an event; PayService consumes it independently.
// Producer (OrderService):
@Service
@RequiredArgsConstructor
public class OrderEventPublisher {
private final KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate;
public void publish(Order order) {
OrderPlacedEvent event = new OrderPlacedEvent(
order.getId(), order.getUserId(), order.getTotalAmount()
);
kafkaTemplate.send("order.placed", event);
// OrderService continues immediately β does not wait for PayService.
}
}
// Consumer (PayService):
@Service
public class PaymentEventListener {
@KafkaListener(topics = "order.placed", groupId = "pay-service")
public void handleOrderPlaced(OrderPlacedEvent event) {
// PayService processes payment independently.
paymentProcessor.charge(event.getUserId(), event.getAmount());
}
}API Gateway
An API Gateway is the single entry point for all client requests. It routes requests to the appropriate downstream service, handles cross-cutting concerns such as authentication, rate limiting, and SSL termination, and shields clients from the internal service topology. Common choices include Spring Cloud Gateway, Kong, and AWS API Gateway.
Java
// ββ Spring Cloud Gateway configuration βββββββββββββββββββββββββββββββ
// pom.xml:
// <dependency>
// <groupId>org.springframework.cloud</groupId>
// <artifactId>spring-cloud-starter-gateway</artifactId>
// </dependency>
// application.yml:
// spring:
// cloud:
// gateway:
// routes:
// - id: user-service
// uri: lb://user-service # 'lb://' = load-balanced via Eureka
// predicates:
// - Path=/api/users/**
// filters:
// - StripPrefix=1
//
// - id: order-service
// uri: lb://order-service
// predicates:
// - Path=/api/orders/**
// filters:
// - StripPrefix=1
// ββ Programmatic route definition: βββββββββββββββββββββββββββββββββββ
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r
.path("/api/users/**")
.filters(f -> f
.stripPrefix(1)
.addRequestHeader("X-Gateway-Source", "api-gateway"))
.uri("lb://user-service"))
.route("order-service", r -> r
.path("/api/orders/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://order-service"))
.build();
}
}
// ββ Client request flow: ββββββββββββββββββββββββββββββββββββββββββββββ
//
// Mobile/Browser
// β
// βΌ
// API Gateway :8080
// β
// βββ /api/users/** βββΆ UserService :8081
// βββ /api/orders/** βββΆ OrderService :8082
// βββ /api/pay/** βββΆ PayService :8083
//
// Client knows only the gateway URL.
// Internal service ports/hosts are never exposed.Service Discovery
In a microservices environment, service instances start and stop dynamically and their IP addresses change. Service discovery solves this by maintaining a registry of live instances. Spring Cloud Netflix Eureka is the most common solution in the Spring ecosystem. Services register themselves on startup and query the registry to find peers.
Java
// ββ Eureka Server (the registry): ββββββββββββββββββββββββββββββββββββ
// pom.xml:
// <dependency>
// <groupId>org.springframework.cloud</groupId>
// <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
// </dependency>
@SpringBootApplication
@EnableEurekaServer // turn this app into the registry
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
// application.yml (Eureka Server):
// server:
// port: 8761
// eureka:
// client:
// register-with-eureka: false # server does not register itself
// fetch-registry: false
// ββ Eureka Client (every microservice): ββββββββββββββββββββββββββββββ
// pom.xml:
// <dependency>
// <groupId>org.springframework.cloud</groupId>
// <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
// </dependency>
@SpringBootApplication
@EnableDiscoveryClient // register with Eureka on startup
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
// application.yml (UserService):
// spring:
// application:
// name: user-service # name used in registry + lb:// URIs
// eureka:
// client:
// service-url:
// defaultZone: http://localhost:8761/eureka/
// ββ Load-balanced RestTemplate via Eureka: βββββββββββββββββββββββββββ
@Configuration
public class AppConfig {
@Bean
@LoadBalanced // resolves lb://service-name via Eureka
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
// Usage in OrderService:
// restTemplate.getForObject("http://user-service/api/users/1", ...)
// Spring resolves "user-service" to a real host:port from the registry.Fault Tolerance with Circuit Breaker
When a downstream service is slow or unavailable, without protection the calling service's threads fill up waiting, causing a cascading failure across the system. The Circuit Breaker pattern monitors call failure rates and, once a threshold is breached, "opens" the circuit β immediately returning a fallback response instead of attempting the failing call. Resilience4j is the standard library for this in Spring Boot 3.
Java
// pom.xml:
// <dependency>
// <groupId>io.github.resilience4j</groupId>
// <artifactId>resilience4j-spring-boot3</artifactId>
// </dependency>
// <dependency>
// <groupId>org.springframework.boot</groupId>
// <artifactId>spring-boot-starter-aop</artifactId>
// </dependency>
// application.yml:
// resilience4j:
// circuitbreaker:
// instances:
// userService:
// sliding-window-size: 10 # evaluate last 10 calls
// failure-rate-threshold: 50 # open if β₯ 50 % fail
// wait-duration-in-open-state: 10s # stay open for 10 s, then half-open
// permitted-number-of-calls-in-half-open-state: 3
// ββ Annotated usage: ββββββββββββββββββββββββββββββββββββββββββββββββββ
@Service
@RequiredArgsConstructor
public class OrderService {
private final RestTemplate restTemplate;
// CircuitBreaker wraps this method.
// fallbackMethod is called when the circuit is open OR the call fails.
@CircuitBreaker(name = "userService", fallbackMethod = "fallbackUser")
public UserResponse getUser(Long userId) {
return restTemplate.getForObject(
"http://user-service/api/users/" + userId,
UserResponse.class
);
}
// Fallback β same return type, extra Throwable parameter:
private UserResponse fallbackUser(Long userId, Throwable ex) {
// Return a cached/default response instead of propagating the error.
return UserResponse.builder()
.id(userId)
.name("Unknown User")
.build();
}
// ββ Circuit states: βββββββββββββββββββββββββββββββββββββββββββββββ
// CLOSED β normal operation; calls pass through; failures counted.
// OPEN β circuit tripped; all calls return fallback immediately.
// HALF-OPEN β after wait, a few test calls are allowed through.
// If they succeed β CLOSED. If they fail β OPEN again.
}