Spring Boot Microservices Orchestration Example
This project demonstrates the orchestration pattern in a Spring Boot microservices architecture,
where a central orchestrator service coordinates the workflow across order, inventory, and payment services. By combining Resilience4j retries on remote service calls with a circuit breaker around the orchestration logic, the system achieves both fault tolerance and stability. The result is a resilient, centralized workflow that ensures business processes either complete successfully or fail fast with clear error handling.Orchestration in microservices architecture is a centralized system that manages and coordinates how multiple individual and independent microservices work together to complete a larger business process.
A sequence of operations (what to do and when) are generally controlled by central service called orchestrator. For example, the individual and independent microservices like inventory service, order service and payment service and sequence of operations performed by these microservices can be controlled by orchestration service.
The orchestration approach in microservices architecture simplifies the complex workflows, makes easier to handle errors and provides a clear picture of the whole process.
I’ll use orchestration (centralized control) where a central service (Orchestrator) coordinates the flow between microservices.
The sequence of actions could be represented as:
Client → API Gateway → Order Service → Orchestrator → Inventory Service → Payment Service.
Tech Stack
- Spring Boot for all services
- Spring REST APIs
- Spring Cloud OpenFeign (for inter-service communication)
- Spring Cloud Eureka Server for service discovery
- Spring Cloud API Gateway for routing requests to services
- Resilience4j for retries/circuit breakers.
- MySQL
- Maven
Orchestration Workflow (Step‑by‑Step)
Client Request
- A user places an order through the Order Service (via API Gateway).
- The order details are forwarded to the Orchestrator Service.
Inventory Check
- Orchestrator calls Inventory Service → /inventory/check.
- If stock is unavailable → Orchestrator returns “Order Failed: Product out of stock.”
Reserve Inventory
- If stock is available, Orchestrator calls Inventory Service → /inventory/reserve.
- If reservation fails → Orchestrator returns “Order Failed: Unable to reserve stock.”
Process Payment
- Orchestrator delegates payment to PaymentInvoker (wrapped with
@Retry). - PaymentInvoker calls Payment Service → /payment/process.
- If payment fails:
- Retry attempts up to 3 times (configurable).
- If all retries fail, exception bubbles up.
Circuit Breaker Protection
- The Orchestrator’s
@CircuitBreakermonitors failures. - If threshold is exceeded, it triggers the fallback method:
- “Order Failed: Payment service unavailable. Please try again later.”
Success Path
- If payment succeeds, Orchestrator returns “Order Placed Successfully!”
Key Resilience Layers
- Retry (PaymentInvoker) → Handles transient errors at the Feign client level.
- Circuit Breaker (OrchestratorService) → Protects the orchestration flow from repeated downstream failures.
- Fallback → Provides a graceful user‑facing error message when services are unavailable.
Step-by-Step Implementation
Order Service
The order microservice publishes /orders endpoint to place an order. The order details are sent to the Orchestrator. This service accepts an order and forwards it to the orchestrator-service.
Controller
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrchestratorClient orchestratorClient;
@PostMapping
public String placeOrder(@RequestBody OrderRequest request) {
return orchestratorClient.processOrder(request);
}
} Orchestrator Client
@FeignClient(name = "orchestrator-service", url = "http://localhost:8084")
public interface OrchestratorClient {
@PostMapping("/orchestrate")
String processOrder(OrderRequest request);
} Inventory Service
The inventory service publishes two endpoints /inventory/check and /inventory/reserve.
The endpoint /inventory/check verifies the stock if a product is available to be ordered or not in stock.
The endpoint /inventory/reserve reserves the stock when an order is placed if the requested product available.
Database Table
CREATE DATABASE IF NOT EXISTS `inventorydb`;
USE `inventorydb`;
-- Dumping structure for table inventorydb.inventory_item
CREATE TABLE IF NOT EXISTS `inventory_item` (
`product_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
`quantity` int unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`product_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; Repository
public interface InventoryRepository extends JpaRepository<InventoryItem, String> {
} Service
@Service
public class InventoryService {
@Autowired
private InventoryRepository repository;
public boolean checkStock(String productId, int quantity) {
Optional<InventoryItem> item = repository.findById(productId);
return item.map(i -> i.getQuantity() >= quantity).orElse(false);
}
public boolean reserveStock(String productId, int quantity) {
Optional<InventoryItem> itemOpt = repository.findById(productId);
if (itemOpt.isPresent()) {
InventoryItem item = itemOpt.get();
if (item.getQuantity() >= quantity) {
item.setQuantity(item.getQuantity() - quantity);
repository.save(item);
return true;
}
}
return false;
}
} Controller
@RestController
@RequestMapping("/inventory")
public class InventoryController {
@Autowired
private InventoryService inventoryService;
@GetMapping("/check")
public boolean checkStock(@RequestParam String productId, @RequestParam int quantity) {
return inventoryService.checkStock(productId, quantity);
}
@PostMapping("/reserve")
public boolean reserveStock(@RequestParam String productId, @RequestParam int quantity) {
return inventoryService.reserveStock(productId, quantity);
}
} Payment Service
The payment service publishes endpoint /payment/process to process the payment for an order.
- Uses
UserAccountentity with balance. - Deducts amount if sufficient funds.
- Stores data in MySQL.
Database Table
CREATE DATABASE IF NOT EXISTS `paymentdb`;
USE `paymentdb`;
-- Dumping structure for table paymentdb.user_account
CREATE TABLE IF NOT EXISTS `user_account` (
`user_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
`balance` double NOT NULL DEFAULT '0',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; Service
@Service
public class PaymentService {
@Autowired
private UserAccountRepository repository;
public boolean processPayment(String userId, double amount) {
Optional<UserAccount> accountOpt = repository.findById(userId);
if (accountOpt.isPresent()) {
UserAccount account = accountOpt.get();
if (account.getBalance() >= amount) {
account.setBalance(account.getBalance() - amount);
repository.save(account);
return true;
}
}
return false;
}
} Controller
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Autowired
private PaymentService paymentService;
@PostMapping("/process")
public boolean processPayment(@RequestParam String userId, @RequestParam double amount) {
return paymentService.processPayment(userId, amount);
}
} Orchestrator Service
The orchestration service is the central point of communication and the orchestrator decides how the multiple services will work together in what sequence of operations to accomplish a larger task in business model.
The orchestrator-service, which will coordinate the flow between the order-service, inventory-service, and payment-service.
So, for this example, the orchestrator calls Inventory → Payment → Confirms Order.
Therefore this service receives the order request and orchestrates the following:
- Check inventory
- Reserve inventory
- Process payment
- Return success/failure
Service
- Uses Resilience4j annotations.
- Retries payment up to 3 times.
- Circuit breaker fallback if service is down.
The following design keeps responsibilities clear and avoids proxy ordering issues.
- Retry on the remote call (Feign client).
- Circuit breaker on the business orchestration.
@Service
public class PaymentInvoker {
@Autowired
private PaymentClient paymentClient;
@Retry(name = "paymentRetry")
public void invokePayment(String userId, double amount) {
paymentClient.processPayment(userId, amount);
}
} @Service
public class OrchestratorService {
@Autowired
private InventoryClient inventoryClient;
@Autowired
private PaymentInvoker paymentInvoker;
@Retry(name = "paymentRetry")
@CircuitBreaker(name = "paymentCircuitBreaker", fallbackMethod = "paymentFallback")
public String processOrder(OrderRequest request) {
boolean inStock = inventoryClient.checkStock(request.getProductId(), request.getQuantity());
if (!inStock)
return "Order Failed: Product out of stock.";
boolean reserved = inventoryClient.reserveStock(request.getProductId(), request.getQuantity());
if (!reserved)
return "Order Failed: Unable to reserve stock.";
paymentInvoker.invokePayment(request.getUserId(), request.getQuantity() * 100);
return "Order Placed Successfully!";
}
// fallback must match the signature of the circuit breaker method
public String paymentFallback(OrderRequest request, Throwable t) {
return "Order Failed: Payment service unavailable. Please try again later.";
}
} Feign Clients
@FeignClient(name = "inventory-service", url = "http://localhost:8082")
public interface InventoryClient {
@GetMapping("/inventory/check")
boolean checkStock(@RequestParam String productId, @RequestParam int quantity);
@PostMapping("/inventory/reserve")
boolean reserveStock(@RequestParam String productId, @RequestParam int quantity);
} @FeignClient(name = "payment-service", url = "http://localhost:8083")
public interface PaymentClient {
@PostMapping("/payment/process")
boolean processPayment(@RequestParam String userId, @RequestParam double amount);
} Controller
@RestController
@RequestMapping("/orchestrate")
public class OrchestratorController {
@Autowired
private OrchestratorService orchestratorService;
@PostMapping
public String orchestrateOrder(@RequestBody OrderRequest request) {
return orchestratorService.processOrder(request);
}
} Spring Cloud Eureka
To enable the service discovery I have used Spring Cloud Eureka.
Eureka Server – this is the central registry where services register themselves.
Eureka Clients – all microservices (order, inventory, payment, orchestrator) register with Eureka.
You may or may not add @EnableEurekaClient annotation for client all microservices as it is optional with Spring Boot 3+.
Application config – application.yml
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
instance:
prefer-ip-address: true
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false Spring Cloud API Gateway
The Spring Cloud Gateway or API Gateway acts as a single entry point and it routes requests to services using Eureka service names.
Application config – application.yml
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/orders/**
- id: inventory-service
uri: lb://inventory-service
predicates:
- Path=/inventory/**
- id: payment-service
uri: lb://payment-service
predicates:
- Path=/payment/**
- id: orchestrator-service
uri: lb://orchestrator-service
predicates:
- Path=/orchestrate/**
eureka:
instance:
prefer-ip-address: true
hostname: localhost
client:
service-url:
defaultZone: http://localhost:8761/eureka Test Flow
Start apps in this order:
- Start eureka-server
- Start all services (they register with Eureka)
- Start api-gateway
Or specifically:
- Eureka Server (eureka-server) – Port 8761
- Inventory Service (inventory-service) – Port 8082
- Payment Service (payment-service) – Port 8083
- Orchestrator Service (orchestrator-service) – Port 8084
- Order Service (order-service) – Port 8081
- API Gateway (api-gateway) – Port 8080
Once all services, server and API gateway are up and microservices register themselves then you will see them under Eureka server registry:
Testing Instructions in Postman
Place Order via API Gateway
URL: http://localhost:8080/orders
Method: POST
Body (JSON):
{
"productId": "P1001",
"quantity": 2,
"userId": "U123"
} Expected Output:
Success: Order Placed Successfully!
Failure: Order Failed: Product out of stock. or any other error message
Check Inventory
URL: http://localhost:8080/inventory/check?productId=P1001&quantity=2
Method: GET
Expected Output: true if stock is available. false if stock is not available.
Reserve Inventory
URL: http://localhost:8080/inventory/reserve?productId=P1001&quantity=2
Method: POST
Expected Output: true if reservation successful. false if not enough stock.
Process Payment
URL: http://localhost:8080/payment/process?userId=U123&amount=200.0
Method: POST
Expected Output: true if payment succeeds. false if it fails.
Direct Orchestration Call
URL: http://localhost:8080/orchestrate
Method: POST
Body (JSON):
{
"productId": "P1001",
"quantity": 2,
"userId": "U123"
} Expected Output: Same as /orders endpoint, since it triggers the orchestrator directly. Here I have got output as Order Placed Successfully!.
Simulate Failure Scenario
In PaymentService.java, temporarily force failure:
public boolean processPayment(String userId, double amount) {
throw new RuntimeException("Simulated payment failure");
} Call Orchestrator Endpoint
URL: http://localhost:8080/orchestrate
Method: POST
Body:
{
"productId": "P1001",
"quantity": 2,
"userId": "U123"
} Expected Behavior: Resilience4j retries 3 times. If all fail, circuit breaker opens.
Order Failed: Payment service unavailable. Please try again later. You will see exception in payment service console:
[Request processing failed: java.lang.RuntimeException: Simulated payment failure] with root cause
java.lang.RuntimeException: Simulated payment failure You will see retry log in orchestration service console:
Retry attempt #1 due to: [500] during [POST] to [http://localhost:8083/payment/process?userId=U123&amount=200.0] [PaymentClient#processPayment(String,double)]: [{"timestamp":"2025-10-22T17:13:12.802+00:00","status":500,"error":"Internal Server Error","path":"/payment/process"}]
Retry attempt #2 due to: [500] during [POST] to [http://localhost:8083/payment/process?userId=U123&amount=200.0] [PaymentClient#processPayment(String,double)]: [{"timestamp":"2025-10-22T17:13:13.840+00:00","status":500,"error":"Internal Server Error","path":"/payment/process"}]
Fallback triggered due to: [500] during [POST] to [http://localhost:8083/payment/process?userId=U123&amount=200.0] [PaymentClient#processPayment(String,double)]: [{"timestamp":"2025-10-22T17:13:14.850+00:00","status":500,"error":"Internal Server Error","path":"/payment/process"}] Conclusion
In a microservices architecture, the orchestration pattern centralizes control of a business workflow in a single service (the orchestrator). This service coordinates calls to other microservices — such as inventory, payment, and order — ensuring the overall process completes successfully or fails gracefully.
By applying resilience patterns like retry and circuit breaker at the right layers, the orchestrator can handle transient failures (via retries) and protect the system from cascading failures (via circuit breakers). The key lesson is that retries should be applied close to the remote call (e.g., the Feign client), while circuit breakers should wrap the orchestration logic. This separation ensures that the orchestrator remains robust, avoids premature fallbacks, and maintains system stability under failure conditions.
In short:
- Orchestration pattern = centralized workflow control.
- Retry = handle transient errors at the service call level.
- Circuit breaker = protect the orchestration flow from repeated downstream failures.
- Separation of concerns = ensures both patterns work harmoniously, delivering a resilient orchestration layer.

No comments
Leave a comment