Microservices with Spring Cloud
Microservices split one big application into small services. This creates new problems: how do services find each other, where does configuration live, how do requests enter the system, and how do you trace one request across services. Spring Cloud provides answers, and Kubernetes provides many of them too.
The building blocks
┌───────────── Config Server (Git) ─────────────┐
│ central configuration for all services │
Client ──► API Gateway ┼─► Order Service ──► Payment Service │
(mobile/web) (routing,│ │ ▲ │ │
auth, rate │ ▼ │ lookup ▼ │
limit) │ Service Registry (Eureka / Consul / Kubernetes DNS)
│ every service registers itself and finds others by NAME
└─ Distributed tracing (trace id) + central logs + metrics| Problem | Spring Cloud tool | Kubernetes alternative |
|---|---|---|
| Service discovery | Eureka, Consul (client side load balancing) | Kubernetes Service and DNS |
| Central configuration | Spring Cloud Config Server | ConfigMap and Secret |
| Single entry point | Spring Cloud Gateway | Ingress or Gateway API, service mesh |
| Resilience | Resilience4j circuit breakers | Service mesh (Istio, Linkerd) policies |
| Tracing | Micrometer Tracing + OpenTelemetry | Same |
| Inter-service calls | RestClient / HTTP interface with @LoadBalanced, OpenFeign | Plain HTTP to the service name |
Version rule: Spring Cloud releases (for example 2025.0.x) match specific Spring Boot lines. Always take the release train version from the official compatibility table for your Spring Boot version, and import the Spring Cloud BOM.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version> <!-- choose from the compatibility table -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>1. Config Server
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication { public static void main(String[] a) { SpringApplication.run(ConfigServerApplication.class, a); } }# config-server application.yml
server.port: 8888
spring.cloud.config.server.git:
uri: https://github.com/mycompany/config-repo
default-label: main
# the repo holds: order-service.yml, order-service-prod.yml, application.yml (shared) ...# in each client service
spring:
application.name: order-service
config.import: optional:configserver:http://localhost:8888
profiles.active: prodThe client loads order-service-prod.yml from Git at startup. To change configuration without a restart, use Spring Cloud Bus or Kubernetes rolling restarts, and mark beans with @RefreshScope.
2. Service discovery with Eureka
@SpringBootApplication
@EnableEurekaServer
public class RegistryApplication { public static void main(String[] a) { SpringApplication.run(RegistryApplication.class, a); } }# each service (client)
eureka:
client:
service-url.defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true@Configuration
public class ClientConfig {
@Bean
@LoadBalanced // "http://payment-service" is resolved by name, with client side load balancing
RestClient.Builder loadBalancedBuilder() { return RestClient.builder(); }
}
@Service
public class PaymentGateway {
private final RestClient client;
public PaymentGateway(@LoadBalanced RestClient.Builder builder) { this.client = builder.baseUrl("http://payment-service").build(); }
public String status(String id) { return client.get().uri("/payments/{id}", id).retrieve().body(String.class); }
}3. API Gateway
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service # lb:// = load balanced by service name
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
- id: payment-service
uri: lb://payment-service
predicates:
- Path=/api/payments/**The gateway is a natural place for authentication (validate the JWT once), CORS, rate limiting, request logging and adding a correlation id. It is built on WebFlux, so never put blocking code in gateway filters.
Patterns you must know
| Pattern | Idea |
|---|---|
| Database per service | Each service owns its data. No shared tables. Other services use the API or events |
| Saga | A business transaction across services is a sequence of local transactions with compensating actions if a step fails (choreography with events, or orchestration by a coordinator) |
| Transactional Outbox | Write the event to an outbox table in the same DB transaction, then publish it reliably |
| API Composition | A gateway or a BFF (backend for frontend) calls several services and merges results |
| CQRS | Separate models for writing and reading, often with events |
| Idempotent consumers | Handle duplicate messages safely |
| Strangler Fig | Move a monolith to microservices piece by piece |
Sync vs async communication
- Synchronous (HTTP): simple, but couples availability. Use timeouts, retries and circuit breakers.
- Asynchronous (Kafka, RabbitMQ): loose coupling and resilience, but eventual consistency and more design work.
- Rule of thumb: queries use HTTP, state changes that other services react to use events.
Common mistake: Starting with microservices for a small team and a small product. You get network failures, distributed transactions and operational cost without the benefit. Start with a well-structured modular monolith and extract services when scaling or team boundaries demand it.
Interview questions
Do I still need Eureka on Kubernetes?
Usually not. Kubernetes gives every Service a DNS name and load balancing, so services can call http://payment-service directly. Config and secrets also come from ConfigMaps and Secrets.
How do I handle a transaction across two services?
You cannot use a database transaction. Use a saga with compensation steps, keep each local transaction small, and make every step idempotent.
What is an API gateway used for?
One entry point for clients: routing, authentication, rate limiting, TLS termination, CORS and response aggregation.