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 @CircuitBreaker monitors 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 UserAccount entity 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:

  1. Check inventory
  2. Reserve inventory
  3. Process payment
  4. 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:

  1. Eureka Server (eureka-server) – Port 8761
  2. Inventory Service (inventory-service) – Port 8082
  3. Payment Service (payment-service) – Port 8083
  4. Orchestrator Service (orchestrator-service) – Port 8084
  5. Order Service (order-service) – Port 8081
  6. 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:

Microservices Orchestration 1

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.

Source Code

Download

Share

No comments

Leave a comment