๐Ÿ‡ฎ๐Ÿ‡ณ
๐Ÿ‡ฎ๐Ÿ‡ณ
Limited-Time Offer!Get 20% OFF on all live courses
Enroll Now
PrakalpanaLive online tech training
Architectureโฑ๏ธ 14 min read๐Ÿ“… Jul 16

Spring Boot Microservices Tutorial: Build a Production-Ready System (2026)

VR
Venkat Raghavanโ€ขPrincipal Architect
๐Ÿ“‘ Contents (11 sections)

๐Ÿ“ŒWhat You Will Build

Most microservices tutorials stop at "hello from service A calling service B." This one builds the pieces you actually ship: service discovery, an API gateway, declarative HTTP clients, fault tolerance, and containerization. We will build a small order system with three services, an order-service, an inventory-service, and a gateway, plus a Eureka registry that ties them together.

If you are new to the Spring ecosystem, first skim our Spring Boot course and the wider Microservices track. This tutorial assumes you already know how to write a basic REST controller.

๐Ÿ“ŒThe Architecture at a Glance

+------------------+
Client --> | API Gateway | (Spring Cloud Gateway, port 8080)
+--------+---------+
|
+----------------+----------------+
| |
+---------------+ +------------------+
| order-service | --Feign--> | inventory-service|
| (port 8081) | | (port 8082) |
+-------+-------+ +---------+--------+
| |
+------------> Eureka <-------------+
(discovery, port 8761)

Every service registers itself with Eureka. The gateway and the Feign clients look services up by name, not by hardcoded host and port. That single decision is what makes the system elastic, you can run three copies of inventory-service and traffic load-balances automatically.

๐Ÿ“ŒStep 1: The Eureka Discovery Server

Create a new Spring Boot project with the spring-cloud-starter-netflix-eureka-server dependency. The server itself is almost no code, just an annotation.

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServerApplication.class, args);
}
}

Its application.yml tells it not to register with itself:

server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
spring:
application:
name: discovery-server

Start it and visit http://localhost:8761, the Eureka dashboard is where every registered instance will appear.

๐Ÿ“ŒStep 2: The Inventory Service

This is a normal Spring Boot service that also registers with Eureka. Add spring-boot-starter-web and spring-cloud-starter-netflix-eureka-client.

@RestController
@RequestMapping("/api/inventory")
public class InventoryController {
@GetMapping("/{sku}")
public InventoryResponse checkStock(@PathVariable String sku) {
// In real life this hits a database
int available = "SKU-123".equals(sku) ? 42 : 0;
return new InventoryResponse(sku, available, available > 0);
}
}
public record InventoryResponse(String sku, int quantity, boolean inStock) {}

The magic is in application.yml, the spring.application.name is how other services will find it:

server:
port: 8082
spring:
application:
name: inventory-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka

Once running, refresh the Eureka dashboard, INVENTORY-SERVICE now shows up as UP.

๐Ÿ“ŒStep 3: The Order Service with a Feign Client

The order-service needs stock data from inventory-service. Instead of hand-writing RestTemplate or WebClient calls, use OpenFeign, a declarative HTTP client. Add spring-cloud-starter-openfeign, enable it, and describe the remote API as a Java interface.

@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
@FeignClient(name = "inventory-service") // resolved via Eureka, no URL!
public interface InventoryClient {
@GetMapping("/api/inventory/{sku}")
InventoryResponse checkStock(@PathVariable("sku") String sku);
}

Notice there is no host or port, Feign asks Eureka where inventory-service lives and load-balances across every registered instance. Now the order logic reads like a local method call:

@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final InventoryClient inventoryClient;
public OrderController(InventoryClient inventoryClient) {
this.inventoryClient = inventoryClient;
}
@PostMapping
public ResponseEntity<String> placeOrder(@RequestBody OrderRequest req) {
InventoryResponse stock = inventoryClient.checkStock(req.sku());
if (!stock.inStock()) {
return ResponseEntity.status(HttpStatus.CONFLICT).body("Out of stock");
}
// persist order...
return ResponseEntity.ok("Order placed for " + req.sku());
}
}

๐Ÿ“ŒStep 4: Fault Tolerance with Resilience4j

What happens when inventory-service is slow or down? Without protection, threads pile up waiting and the failure cascades to order-service. Resilience4j adds a circuit breaker: after a threshold of failures it "opens" and fails fast, calling a fallback instead of hammering a dead service.

Add spring-cloud-starter-circuitbreaker-resilience4j, then annotate the call:

@Service
public class InventoryService {
private final InventoryClient client;
public InventoryService(InventoryClient client) {
this.client = client;
}
@CircuitBreaker(name = "inventory", fallbackMethod = "fallback")
@Retry(name = "inventory")
public InventoryResponse check(String sku) {
return client.checkStock(sku);
}
// Signature = original params + the thrown Throwable
public InventoryResponse fallback(String sku, Throwable t) {
return new InventoryResponse(sku, 0, false); // safe default
}
}

Tune the breaker in application.yml:

resilience4j:
circuitbreaker:
instances:
inventory:
sliding-window-size: 10
failure-rate-threshold: 50
wait-duration-in-open-state: 10s

Now if half of the last 10 calls fail, the breaker opens for 10 seconds and every request instantly returns the fallback, your order flow degrades gracefully instead of collapsing. Circuit breakers, bulkheads, and retries are core to real system design; we go deep on them in the High Level Design course.

๐Ÿ“ŒStep 5: The API Gateway

Clients should not talk to five services on five ports. Spring Cloud Gateway gives you one front door with routing, auth, rate limiting, and CORS. Add spring-cloud-starter-gateway and configure routes that resolve through Eureka using the lb:// (load-balanced) scheme:

server:
port: 8080
spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
- id: inventory-service
uri: lb://inventory-service
predicates:
- Path=/api/inventory/**
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka

Now POST http://localhost:8080/api/orders routes to the order service and GET http://localhost:8080/api/inventory/SKU-123 routes to inventory, all behind one port.

๐Ÿ“ŒStep 6: Dockerize Everything

Each service gets a small, layered Dockerfile. Build a JAR first (mvn clean package), then:

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/order-service-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]

Then orchestrate the whole system with docker-compose.yml. The key change inside containers: services find Eureka by its service name, not localhost.

services:
discovery:
build: ./discovery-server
ports: ["8761:8761"]
inventory:
build: ./inventory-service
environment:
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery:8761/eureka
depends_on: [discovery]
order:
build: ./order-service
environment:
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery:8761/eureka
depends_on: [discovery]
gateway:
build: ./gateway
ports: ["8080:8080"]
environment:
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery:8761/eureka
depends_on: [discovery]

Run docker compose up --build and the entire stack, discovery, two business services, and the gateway, comes up together.

๐Ÿ“ŒProduction Checklist

Before you call this "production-ready," add these:

ConcernTool

Centralized configSpring Cloud Config / environment variables Distributed tracingMicrometer Tracing + Zipkin/Tempo Metrics & healthSpring Boot Actuator + Prometheus AuthJWT validation at the gateway LoggingStructured JSON logs shipped to a central store Async communicationKafka/RabbitMQ for events instead of only sync calls

๐Ÿ“ŒWhere to Go Next

You now have the backbone of a real microservices system. The next skills are event-driven communication and designing for scale, both covered in the Microservices course and the System Design track. Also strengthen your Java foundation via the Java track so concurrency and memory never surprise you in production.

๐Ÿ“ŒLearn It Live

Building this yourself is the fastest way to internalize it, but a mentor catching your mistakes in real time is faster. Prakalpana runs live online and 1-on-1 microservices training where you ship this exact system with review at every step. WhatsApp or call +91 9945619267 to book a free counselling session.

VR

Written by

Venkat Raghavan

Principal Architect

๐Ÿš€ Master Architecture

Join 5000+ developers

Explore Courses โ†’