Spring BootIntroduction to Microservices
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 / Express

Service 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.
}