Skip to main content
Technology & EngineeringJava Spring359 lines

Spring Cloud

Microservices architecture with Spring Cloud including service discovery, API gateway, circuit breakers, and distributed configuration

Quick Summary16 lines
You are an expert in building microservices architectures with Spring Cloud and Spring Boot. You treat distributed systems as inherently unreliable and design every inter-service interaction with failure, latency, and partial availability as expected conditions rather than edge cases.

## Key Points

- **Design for failure** — every remote call can fail. Use circuit breakers, retries with exponential backoff, timeouts, and fallbacks.
- **Use an API gateway** — centralize cross-cutting concerns like authentication, rate limiting, and logging at the gateway rather than duplicating them in each service.
- **Externalize configuration** — use Spring Cloud Config or environment variables. Do not bake environment-specific values into service JARs.
- **Implement health checks** — expose `/actuator/health` with liveness and readiness probes for Kubernetes or your orchestration platform.
- **Use correlation IDs** — propagate trace IDs across service calls for end-to-end request tracing. Micrometer Tracing does this automatically.
- **Prefer asynchronous communication** — use event-driven patterns (Kafka, RabbitMQ) for operations that do not need an immediate response. This decouples services and improves resilience.
- **No circuit breaker** — a single failing downstream service causes cascading failures across the entire system. Always wrap remote calls with circuit breakers.
- **Synchronous chains** — Service A calls B, which calls C, which calls D. Latency compounds and any failure in the chain fails the entire request. Break long chains with async events.
- **Shared databases** — two services reading and writing the same tables couples them at the data level. Each service should own its data store.
- **Ignoring network partitions** — services will lose connectivity. Design for eventual consistency and handle partial failures gracefully.
skilldb get java-spring-skills/Spring CloudFull skill: 359 lines
Paste into your CLAUDE.md or agent config

Spring Cloud — Java/Spring Boot

You are an expert in building microservices architectures with Spring Cloud and Spring Boot. You treat distributed systems as inherently unreliable and design every inter-service interaction with failure, latency, and partial availability as expected conditions rather than edge cases.

Core Philosophy

Microservices are a deployment strategy, not an architecture goal. The decision to split a system into independently deployable services should be driven by concrete organizational or scaling needs, not by a desire to use fashionable technology. Every service boundary introduces network latency, serialization overhead, partial failure modes, and operational complexity. If two services must be deployed together to function, they are a distributed monolith with all the costs of microservices and none of the benefits. Start with a well-structured monolith and extract services only when the pain of coupling exceeds the pain of distribution.

Every remote call will eventually fail. Circuit breakers, retries, timeouts, and fallbacks are not defensive extras -- they are mandatory infrastructure for any system that makes network calls. A service without a circuit breaker on its outbound calls is one slow dependency away from a cascading failure that takes down the entire platform. Spring Cloud's Resilience4j integration makes these patterns straightforward to implement, but the team must invest in configuring sensible thresholds and testing failure scenarios. A circuit breaker with default settings that never trips is worse than no circuit breaker at all, because it provides false confidence.

Observability in a distributed system is exponentially harder than in a monolith. A single user request may touch five services, three databases, and two message brokers. Without distributed tracing, correlation IDs, and centralized logging, debugging production issues becomes a multi-hour forensic exercise. Spring Cloud's tracing integration propagates context automatically, but the team must ensure that every service participates, that sampling rates are appropriate, and that trace data flows to a system where it can be searched and visualized. Observability is not a feature to add later; it is a prerequisite for operating microservices responsibly.

Overview

Spring Cloud provides tools for building distributed systems and microservices on the JVM. It offers patterns like service discovery, client-side load balancing, circuit breakers, distributed configuration, and API gateways. Spring Cloud builds on Spring Boot, so each microservice is a standalone Boot application that participates in a larger ecosystem.

Core Concepts

Service Discovery with Eureka

A service registry allows microservices to find each other by logical name rather than hardcoded URLs.

Eureka Server:

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(DiscoveryServerApplication.class, args);
    }
}
# discovery-server application.yml
server:
  port: 8761
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false

Eureka Client (each microservice):

spring:
  application:
    name: order-service
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

API Gateway with Spring Cloud Gateway

@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - StripPrefix=1
            - name: CircuitBreaker
              args:
                name: orderCircuitBreaker
                fallbackUri: forward:/fallback/orders

        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/api/products/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20

Custom Gateway Filter

@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {

    private final JwtTokenProvider tokenProvider;

    public AuthenticationFilter(JwtTokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();

        if (isPublicEndpoint(path)) {
            return chain.filter(exchange);
        }

        String token = extractToken(exchange.getRequest());
        if (token == null || !tokenProvider.validateToken(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        String userId = tokenProvider.extractUserId(token);
        ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
                .header("X-User-Id", userId)
                .build();

        return chain.filter(exchange.mutate().request(mutatedRequest).build());
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

Circuit Breaker with Resilience4j

@Service
public class ProductService {

    private final WebClient webClient;

    public ProductService(WebClient.Builder builder) {
        this.webClient = builder.baseUrl("http://product-service").build();
    }

    @CircuitBreaker(name = "productService", fallbackMethod = "getProductFallback")
    @Retry(name = "productService")
    @TimeLimiter(name = "productService")
    public CompletableFuture<ProductDTO> getProduct(String productId) {
        return webClient.get()
                .uri("/api/products/{id}", productId)
                .retrieve()
                .bodyToMono(ProductDTO.class)
                .toFuture();
    }

    private CompletableFuture<ProductDTO> getProductFallback(String productId, Throwable t) {
        return CompletableFuture.completedFuture(
                new ProductDTO(productId, "Unavailable", BigDecimal.ZERO));
    }
}
resilience4j:
  circuitbreaker:
    instances:
      productService:
        sliding-window-size: 10
        failure-rate-threshold: 50
        wait-duration-in-open-state: 10s
        permitted-number-of-calls-in-half-open-state: 3
  retry:
    instances:
      productService:
        max-attempts: 3
        wait-duration: 500ms
  timelimiter:
    instances:
      productService:
        timeout-duration: 3s

Implementation Patterns

Distributed Configuration with Config Server

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}
# Config server application.yml
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/myorg/config-repo
          default-label: main
          search-paths: '{application}'

Client-side:

# bootstrap.yml in each microservice
spring:
  cloud:
    config:
      uri: http://localhost:8888
      fail-fast: true
      retry:
        max-attempts: 5

Inter-Service Communication with OpenFeign

@FeignClient(name = "inventory-service", fallbackFactory = InventoryClientFallbackFactory.class)
public interface InventoryClient {

    @GetMapping("/api/inventory/{sku}")
    InventoryResponse checkStock(@PathVariable String sku);

    @PostMapping("/api/inventory/{sku}/reserve")
    ReservationResponse reserve(@PathVariable String sku, @RequestBody ReserveRequest request);
}

@Component
public class InventoryClientFallbackFactory implements FallbackFactory<InventoryClient> {

    @Override
    public InventoryClient create(Throwable cause) {
        return new InventoryClient() {
            @Override
            public InventoryResponse checkStock(String sku) {
                return new InventoryResponse(sku, 0, false);
            }

            @Override
            public ReservationResponse reserve(String sku, ReserveRequest request) {
                throw new ServiceUnavailableException("Inventory service is unavailable", cause);
            }
        };
    }
}

Distributed Tracing

# Each microservice
management:
  tracing:
    sampling:
      probability: 1.0 # 100% in dev, lower in production
  zipkin:
    tracing:
      endpoint: http://zipkin:9411/api/v2/spans
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
</dependency>

Event-Driven Communication with Spring Cloud Stream

@Configuration
public class OrderEventConfig {

    @Bean
    public Supplier<OrderEvent> orderCreatedSupplier() {
        // Polled by the framework
        return () -> pendingEvents.poll();
    }

    @Bean
    public Consumer<PaymentEvent> paymentResultConsumer(OrderService orderService) {
        return event -> {
            if (event.isSuccessful()) {
                orderService.confirmOrder(event.getOrderId());
            } else {
                orderService.cancelOrder(event.getOrderId(), event.getReason());
            }
        };
    }
}
spring:
  cloud:
    stream:
      bindings:
        orderCreatedSupplier-out-0:
          destination: order-events
          content-type: application/json
        paymentResultConsumer-in-0:
          destination: payment-results
          group: order-service
      kafka:
        binder:
          brokers: localhost:9092

Best Practices

  • Design for failure — every remote call can fail. Use circuit breakers, retries with exponential backoff, timeouts, and fallbacks.
  • Use an API gateway — centralize cross-cutting concerns like authentication, rate limiting, and logging at the gateway rather than duplicating them in each service.
  • Externalize configuration — use Spring Cloud Config or environment variables. Do not bake environment-specific values into service JARs.
  • Implement health checks — expose /actuator/health with liveness and readiness probes for Kubernetes or your orchestration platform.
  • Use correlation IDs — propagate trace IDs across service calls for end-to-end request tracing. Micrometer Tracing does this automatically.
  • Prefer asynchronous communication — use event-driven patterns (Kafka, RabbitMQ) for operations that do not need an immediate response. This decouples services and improves resilience.

Common Pitfalls

  • Distributed monolith — breaking a monolith into microservices that are tightly coupled and must be deployed together. If services cannot be deployed independently, reconsider service boundaries.
  • No circuit breaker — a single failing downstream service causes cascading failures across the entire system. Always wrap remote calls with circuit breakers.
  • Synchronous chains — Service A calls B, which calls C, which calls D. Latency compounds and any failure in the chain fails the entire request. Break long chains with async events.
  • Shared databases — two services reading and writing the same tables couples them at the data level. Each service should own its data store.
  • Ignoring network partitions — services will lose connectivity. Design for eventual consistency and handle partial failures gracefully.

Anti-Patterns

  • The distributed monolith — splitting a monolith into services that share a database, deploy together, and fail together. This architecture has all the operational complexity of microservices with none of the independence benefits. If services cannot be deployed, scaled, and failed independently, they should not be separate services.

  • Synchronous call chains — Service A calls B, which calls C, which calls D, each waiting for a response. Latency compounds multiplicatively, and any failure in the chain fails the entire request. Replace deep synchronous chains with asynchronous event-driven patterns or aggregate data at the gateway layer.

  • Circuit breakers with untested defaults — adding @CircuitBreaker annotations without tuning thresholds, testing failure scenarios, or monitoring circuit state. A circuit breaker that never opens provides false confidence. Regularly test that circuits open under realistic failure conditions and that fallbacks return useful responses.

  • Shared libraries as coupling vectors — publishing a shared library with domain models, DTOs, and business logic that every service depends on. Changes to the shared library force coordinated deployments across all consumers. Share only contracts (API schemas, event schemas) and keep domain logic private to each service.

  • Configuration drift across environments — managing configuration differently in each environment without Spring Cloud Config or an equivalent centralized system. When staging and production diverge in non-obvious ways, bugs that only manifest in production become common and hard to reproduce.

Install this skill directly: skilldb add java-spring-skills

Get CLI access →