- 1. What Is Spring Boot Microservices Architecture?
- 2. Service Structure & Project Layout
- 3. How Do You Set Up Service Discovery?
- 4. How Do Microservices Communicate?
- 5. Resilience Patterns with Resilience4j
- 6. What Is the Best API Gateway?
- 7. How Do You Monitor Spring Boot Microservices?
- 8. Deployment: Docker & Kubernetes
- 9. Spring Boot Microservices Best Practices 2026
- 10. FAQ
Spring Boot microservices remain the dominant architecture for Java backends in 2026. Building microservices with Spring Boot gives you a mature, opinionated foundation — autoconfigured HTTP servers, production-ready health checks, and the entire Spring Cloud ecosystem for the hard distributed systems problems: service discovery, configuration management, circuit breakers, and distributed tracing. This guide covers every layer of a production Spring Boot microservices architecture, with code you can use today.
At Pillai Infotech, we build and staff Java microservices teams for banking, insurance, and large-scale B2B platforms. What follows is the reference we give our own Java developers when they start a new microservices project.
What Is Spring Boot Microservices Architecture?
A Spring Boot microservices architecture decomposes a system into small, independently deployable services, each owning its own data store and exposing a well-defined API. Spring Boot provides the runtime for each service; Spring Cloud provides the infrastructure glue between them.
The difference between Spring Boot and Spring Cloud is worth stating clearly: Spring Boot is the application framework — embedded Tomcat, autoconfiguration, starters. Spring Cloud is the distributed systems toolkit — Eureka for service discovery, Spring Cloud Gateway for routing, Resilience4j for circuit breaking, Micrometer for observability. You use both together.
| Concern | Spring Cloud Solution | Kubernetes-native Alternative |
|---|---|---|
| Service Discovery | Eureka / Consul | Kubernetes DNS |
| Config Management | Spring Cloud Config | ConfigMaps / Vault |
| Circuit Breaker | Resilience4j | Istio service mesh |
| API Gateway | Spring Cloud Gateway | Kong / Nginx Ingress |
| Distributed Tracing | Micrometer Tracing + OTLP | OpenTelemetry Collector |
| Event Streaming | Spring Cloud Stream + Kafka | Kafka client directly |
If you need help designing the right microservices architecture for your system, see our technology roadmap consulting service — we help engineering leads choose the right stack before a line of code is written.
Service Structure and Project Layout
Every Spring Boot microservice should be independently buildable, testable, and deployable. Keep each service's scope narrow — one bounded context, one database schema, one deployment unit. Here is the standard layout we use across all Spring Boot microservices projects:
order-service/
├── src/main/java/com/example/orders/
│ ├── OrderServiceApplication.java
│ ├── controller/
│ │ └── OrderController.java // REST endpoints
│ ├── service/
│ │ └── OrderService.java // Business logic
│ ├── repository/
│ │ └── OrderRepository.java // Data access (JPA)
│ ├── model/
│ │ ├── Order.java // Entity
│ │ └── OrderDTO.java // Transfer object
│ ├── client/
│ │ └── InventoryClient.java // Feign client to another service
│ ├── config/
│ │ └── SecurityConfig.java
│ └── exception/
│ └── GlobalExceptionHandler.java
├── src/main/resources/
│ ├── application.yml
│ └── application-prod.yml
├── Dockerfile
└── pom.xml
The client/ package holds all outbound Feign clients — it is the only place your service calls other services. This keeps inter-service communication explicit and auditable. The exception/ package contains a single @RestControllerAdvice that maps domain exceptions to HTTP status codes uniformly.
// OrderController.java
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public OrderDTO createOrder(@Valid @RequestBody CreateOrderRequest request) {
return orderService.createOrder(request);
}
@GetMapping("/{id}")
public OrderDTO getOrder(@PathVariable Long id) {
return orderService.getOrder(id);
}
@GetMapping
public Page<OrderDTO> listOrders(Pageable pageable) {
return orderService.listOrders(pageable);
}
}
Note the use of @Valid on request bodies — every inbound payload is validated at the controller layer before reaching service logic. Never trust data from other services without validation; in a microservices architecture, every service is a public API boundary.
How Do You Set Up Service Discovery in Spring Boot?
Service discovery in Spring Boot microservices solves the problem of services finding each other dynamically as instances scale up and down. There are two mainstream approaches: Eureka (Spring Cloud's built-in registry) and Kubernetes DNS (if you are already on K8s).
Option 1 — Eureka (non-K8s environments):
# application.yml — Eureka client
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: http://eureka-server:8761/eureka/
instance:
prefer-ip-address: true
lease-renewal-interval-in-seconds: 10
lease-expiration-duration-in-seconds: 30
Option 2 — Kubernetes DNS (recommended on K8s):
# application.yml — Kubernetes-native discovery
spring:
cloud:
kubernetes:
discovery:
enabled: true
config:
enabled: true # reads from ConfigMaps
With either option, OpenFeign lets you call other services by name — Spring resolves the actual hostname at runtime:
// Feign client — calls inventory-service by logical name
@FeignClient(name = "inventory-service")
public interface InventoryClient {
@GetMapping("/api/inventory/{productId}")
InventoryResponse checkStock(@PathVariable String productId);
}
// Spring resolves "inventory-service" via Eureka or K8s DNS at runtime
How Do Microservices Communicate in Spring Boot?
Spring Boot microservices communication falls into two categories: synchronous (the caller waits for a response) and asynchronous (the caller publishes an event and moves on). Choosing the wrong model is one of the most common architectural mistakes in Spring Boot microservices projects.
| Pattern | Spring Tool | Use When |
|---|---|---|
| Sync REST | OpenFeign / RestClient | Caller needs the response to proceed |
| Sync gRPC | grpc-spring-boot-starter | High-throughput, low-latency internal APIs |
| Async Events | Spring Cloud Stream + Kafka | Fire-and-forget, event sourcing, fanout |
| Async Commands | RabbitMQ / ActiveMQ | Task queues, work distribution |
The governing rule: default to async event-driven communication. Use synchronous calls only where the caller genuinely cannot proceed without the response. Every synchronous dependency creates a coupling that can cascade into a full outage when the downstream service slows down. In a Spring Boot microservices architecture built around Kafka, a single slow consumer does not stall the publisher.
// Event-driven with Spring Cloud Stream + Kafka
@Service
@RequiredArgsConstructor
public class OrderService {
private final StreamBridge streamBridge;
public OrderDTO createOrder(CreateOrderRequest request) {
Order order = orderRepository.save(toEntity(request));
// Publish event — inventory-service consumes async, no coupling
streamBridge.send("order-created",
new OrderCreatedEvent(order.getId(), order.getItems()));
return toDTO(order);
}
}
// Consumer in inventory-service — completely decoupled
@Bean
public Consumer<OrderCreatedEvent> orderCreated() {
return event -> inventoryService.reserveStock(event);
}
For distributed transactions across services, avoid two-phase commit. Use the Saga pattern instead — either choreography (each service reacts to events and publishes the next) or orchestration (a coordinator service drives the sequence). Spring Cloud Stream with Kafka is the standard implementation vehicle for both. See our microservices architecture guide for a full Saga implementation walkthrough.
Resilience Patterns with Resilience4j
No Spring Boot microservices architecture is complete without resilience at the call boundary. Resilience4j is the standard library since Hystrix reached end-of-life. It gives you circuit breakers, retries, rate limiters, and time limiters — all composable via annotations.
@CircuitBreaker(name = "inventory", fallbackMethod = "fallbackStock")
@Retry(name = "inventory", fallbackMethod = "fallbackStock")
@TimeLimiter(name = "inventory")
public CompletableFuture<InventoryResponse> checkStock(String productId) {
return CompletableFuture.supplyAsync(
() -> inventoryClient.checkStock(productId));
}
public CompletableFuture<InventoryResponse> fallbackStock(
String productId, Throwable t) {
log.warn("Inventory service unavailable for product {}", productId, t);
return CompletableFuture.completedFuture(
InventoryResponse.unknown());
}
# application.yml — Resilience4j configuration
resilience4j:
circuitbreaker:
instances:
inventory:
slidingWindowSize: 10
failureRateThreshold: 50 # open circuit at 50% failures
waitDurationInOpenState: 10s
permittedNumberOfCallsInHalfOpenState: 3
retry:
instances:
inventory:
maxAttempts: 3
waitDuration: 500ms
exponentialBackoffMultiplier: 2
retryExceptions:
- java.io.IOException
- feign.RetryableException
timelimiter:
instances:
inventory:
timeoutDuration: 3s
The annotation order matters: @TimeLimiter wraps @Retry which wraps @CircuitBreaker. A timeout triggers a retry; repeated retries that fail open the circuit. This layered behaviour is intentional — do not reorder the annotations.
What Is the Best API Gateway for Spring Boot Microservices?
Spring Cloud Gateway is the recommended API gateway for Spring Boot microservices in 2026. It is built on Project Reactor (non-blocking), integrates natively with Eureka and Spring Security, and supports reactive rate limiting backed by Redis. If your team is already in the Spring ecosystem, Spring Cloud Gateway has zero onboarding friction.
The main alternative is Kong (for teams that need plugin-driven gateway logic managed outside Java) or Nginx Ingress (for K8s teams who want minimal complexity). For most Spring Boot microservices deployments, Spring Cloud Gateway is the right answer.
# Spring Cloud Gateway — routing + circuit breaker + rate limiting
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- name: CircuitBreaker
args:
name: orderCB
fallbackUri: forward:/fallback/orders
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 50
redis-rate-limiter.burstCapacity: 100
redis-rate-limiter.requestedTokens: 1
- id: inventory-service
uri: lb://inventory-service
predicates:
- Path=/api/inventory/**
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
- name: Retry
args:
retries: 2
statuses: BAD_GATEWAY, SERVICE_UNAVAILABLE
The gateway is also where you centralise cross-cutting concerns: JWT validation, request logging, CORS, and global rate limiting. Keeping these out of individual services keeps each microservice focused on business logic.
If your project involves building a multi-service backend from scratch, our team provides custom software development services including architecture, implementation, and handoff to your in-house team.
How Do You Monitor Spring Boot Microservices?
Monitoring Spring Boot microservices requires three pillars: metrics (what is happening), traces (where time is being spent across services), and logs (what went wrong and why). Spring Boot 3.x makes all three first-class through Spring Boot Actuator and Micrometer.
Metrics with Prometheus and Grafana:
# application.yml — expose metrics and tracing
management:
endpoints:
web:
exposure:
include: health, prometheus, info, metrics
endpoint:
health:
show-details: always
tracing:
sampling:
probability: 1.0 # 100% in dev; 0.1 in production
otlp:
tracing:
endpoint: http://otel-collector:4318/v1/traces
metrics:
export:
prometheus:
enabled: true
Distributed tracing with Micrometer + Jaeger:
// Add to pom.xml
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
// Trace IDs propagate automatically through Feign clients and RestTemplate.
// In logs, configure Logback to include traceId and spanId:
// logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]
In production, set tracing.sampling.probability to 0.1 (10% of requests) or use head-based sampling at the OpenTelemetry Collector. Sampling 100% at scale generates more telemetry data than most teams can store affordably.
The recommended observability stack for Spring Boot microservices in 2026: Prometheus + Grafana for metrics dashboards, Jaeger or Tempo for distributed tracing, and Loki for log aggregation. All three integrate with Grafana, giving you a single pane of glass across your entire microservices deployment.
See our observability guide for the full monitoring stack setup with Docker Compose.
Deployment: Docker and Kubernetes
Deploying Spring Boot microservices with Docker is standard. Use multi-stage builds to keep image sizes small and use a non-root user for security compliance:
# Multi-stage Dockerfile for Spring Boot microservices
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY . .
RUN ./mvnw package -DskipTests
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder /app/target/*.jar app.jar
USER app
EXPOSE 8080
ENTRYPOINT ["java", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"-jar", "app.jar"]
The JVM flags are important: -XX:+UseContainerSupport tells the JVM to respect cgroup memory limits (critical in Kubernetes with resource quotas), and -XX:MaxRAMPercentage=75.0 leaves 25% headroom for OS overhead.
Kubernetes deployment for a Spring Boot microservice:
# k8s/order-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 2
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: your-registry/order-service:latest
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 15
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Spring Boot 3.2+ supports GraalVM native images — startup drops from ~2 seconds to ~50ms, memory from ~250MB to ~50MB. This is worth the compilation overhead for serverless or cost-sensitive deployments. For standard long-running Kubernetes pods, the JVM with CDS (Class Data Sharing) is still the more practical choice.
See our Docker containerization guide and CI/CD pipeline guide for deployment automation patterns.
Spring Boot Microservices Best Practices for 2026
These are the decisions that separate Spring Boot microservices that scale cleanly from those that become unmaintainable within 18 months. The Spring Boot microservices best practices that matter most in 2026:
1. Domain-driven service boundaries. Size services by business capability, not by technical layer. An "order-service" is right; a "database-service" is not. Each service owns its schema — no shared databases between services. If two services query the same table, that is a boundary design problem, not a database problem.
2. API versioning from day one. Version your REST APIs under /api/v1/ from the first commit. Changing API contracts without versioning in a microservices architecture breaks all consumers simultaneously.
3. Externalize all configuration. No hard-coded environment-specific values in code. Use Spring Cloud Config or Kubernetes ConfigMaps. The same JAR artifact should run in dev, staging, and production with only configuration differences.
4. Design for failure, not for the happy path. Every synchronous call to another service will eventually fail. Every Feign client needs a fallback. Every operation that modifies external state needs an idempotency key. Use Resilience4j circuit breakers on all synchronous inter-service calls without exception.
5. Structured, correlated logging. Every log entry must include the trace ID and span ID from Micrometer. Without correlation IDs, debugging a failure that spans five services becomes a multi-hour manual trace through log files.
6. Consumer-driven contract testing. Use Pact or Spring Cloud Contract to verify that your Feign clients match what downstream services actually provide. Integration tests against a mock are insufficient — they test what you think the API does, not what it actually does.
7. Start smaller than you think. The most common Spring Boot microservices mistake is over-decomposing too early. A modular monolith with clean package boundaries is easier to extract services from than a poorly-bounded set of 20 microservices with excessive synchronous coupling. Extract a service when you have a concrete deployment-independence reason, not as a default.
If your backend team is scaling and you need experienced Spring Cloud engineers, our backend developers are available for both short-term delivery and long-term team augmentation.