Careeroza
Order Placement Flow
Step by Step
- App sends
POST /api/orderswith a JWT and anIdempotency-Key. The gateway validates the token and routes to order-service. - order-service checks the idempotency key, reads current prices from product-service (RestClient with timeout and circuit breaker), and in one local transaction saves the order as
PENDING, the saga state and aReserveStockcommand in the outbox. It returns202 Accepted. - The outbox relay publishes the command. inventory-service reserves stock and replies
StockReserved(via its own outbox). - The orchestrator sends
ChargePayment. payment-service captures the UPI/card payment with Razorpay (idempotent on the order ID) and repliesPaymentCompleted. - The orchestrator marks the order
CONFIRMEDand publishesOrderConfirmed. notification-service sends the SMS and email. - The app polls
GET /api/orders/{id}(or receives a push notification) and shows "Order confirmed".
Example
curl -X POST https://api.shopease.in/api/orders \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Idempotency-Key: 5b1e7c2a-91f0-4d3e-a7b2-0c9d8e7f6a51" \
-H "Content-Type: application/json" \
-d '{"items":[{"productId":1,"quantity":2}],"addressId":12,"paymentMethod":"UPI"}'
curl -s https://api.shopease.in/api/orders/1001 -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."Example output
HTTP/1.1 202 Accepted
Location: /api/orders/1001
X-Correlation-Id: 5b1e7c2a-91f0-4d3e-a7b2-0c9d8e7f6a51
{"orderId":1001,"status":"PENDING","total":2998.00}
HTTP/1.1 200 OK
{"orderId":1001,"status":"CONFIRMED","total":2998.00,"items":[{"productId":1,"name":"boAt Rockerz 450","quantity":2,"price":1499.00}],"paymentId":"pay_NqX81kLm2"}Example: The Whole Flow in One Program
Example
A compact simulation of the capstone flow: idempotency at the entry, an orchestrated saga with compensation, outbox events and a notification consumer. The saga runs synchronously here so the output is easy to follow; in production it is asynchronous and the API answers 202 PENDING first.
import java.util.*;
public class Main {
static int stock = 10, wallet = 5000, nextOrder = 1001;
static final Map<String, String> idempotency = new HashMap<>();
static final Map<Integer, String> orders = new TreeMap<>();
static final List<String> outbox = new ArrayList<>();
public static void main(String[] args) {
System.out.println(placeOrder("key-A", 2, 1499));
System.out.println(placeOrder("key-A", 2, 1499)); // client retry after a timeout
System.out.println(placeOrder("key-B", 20, 1499)); // more than the stock
wallet = 1000;
System.out.println(placeOrder("key-C", 1, 3999)); // payment declined
System.out.println("orders=" + orders + " stock=" + stock + " wallet=Rs." + wallet);
relayOutbox();
}
static String placeOrder(String key, int qty, int price) {
if (idempotency.containsKey(key)) return " -> 200 " + idempotency.get(key) + " (replayed, nothing charged)";
int id = nextOrder++;
orders.put(id, "PENDING");
log("order-service", "order " + id + " PENDING, Rs." + qty * price + " (saga started)");
String status = saga(qty, qty * price);
orders.put(id, status);
outbox.add((status.equals("CONFIRMED") ? "OrderConfirmed" : "OrderCancelled") + "#" + id);
String body = "{orderId:" + id + ", status:" + status + "}";
idempotency.put(key, body);
return " -> 201 " + body;
}
static String saga(int qty, int amount) {
if (qty > stock) { log("inventory-service", "StockRejected: only " + stock + " left"); return "CANCELLED"; }
stock -= qty;
log("inventory-service", "StockReserved " + qty);
if (amount > wallet) {
log("payment-service", "PaymentDeclined: balance Rs." + wallet);
stock += qty;
log("inventory-service", "StockReleased " + qty + " (compensation)");
return "CANCELLED";
}
wallet -= amount;
log("payment-service", "PaymentCompleted Rs." + amount);
return "CONFIRMED";
}
static void relayOutbox() {
for (String event : outbox) {
log("outbox-relay", "order-events <- " + event);
String[] p = event.split("#");
log("notification-svc", "SMS: Your ShopEase order " + p[1]
+ (p[0].equals("OrderConfirmed") ? " is confirmed." : " was cancelled. Any amount paid will be refunded."));
}
}
static void log(String service, String message) {
System.out.printf(" [%-17s] %s%n", service, message);
}
}Output
[order-service ] order 1001 PENDING, Rs.2998 (saga started)
[inventory-service] StockReserved 2
[payment-service ] PaymentCompleted Rs.2998
-> 201 {orderId:1001, status:CONFIRMED}
-> 200 {orderId:1001, status:CONFIRMED} (replayed, nothing charged)
[order-service ] order 1002 PENDING, Rs.29980 (saga started)
[inventory-service] StockRejected: only 8 left
-> 201 {orderId:1002, status:CANCELLED}
[order-service ] order 1003 PENDING, Rs.3999 (saga started)
[inventory-service] StockReserved 1
[payment-service ] PaymentDeclined: balance Rs.1000
[inventory-service] StockReleased 1 (compensation)
-> 201 {orderId:1003, status:CANCELLED}
orders={1001=CONFIRMED, 1002=CANCELLED, 1003=CANCELLED} stock=8 wallet=Rs.1000
[outbox-relay ] order-events <- OrderConfirmed#1001
[notification-svc ] SMS: Your ShopEase order 1001 is confirmed.
[outbox-relay ] order-events <- OrderCancelled#1002
[notification-svc ] SMS: Your ShopEase order 1002 was cancelled. Any amount paid will be refunded.
[outbox-relay ] order-events <- OrderCancelled#1003
[notification-svc ] SMS: Your ShopEase order 1003 was cancelled. Any amount paid will be refunded.The Order Endpoint
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final PlaceOrderService placeOrder;
@PostMapping
@PreAuthorize("hasAuthority('SCOPE_orders:write')")
public ResponseEntity<OrderResponse> place(@RequestHeader("Idempotency-Key") String key,
@Valid @RequestBody PlaceOrderRequest req,
@AuthenticationPrincipal Jwt jwt) {
OrderResponse order = placeOrder.place(jwt.getSubject(), key, req); // idempotent
return ResponseEntity.accepted()
.location(URI.create("/api/orders/" + order.orderId()))
.body(order); // status PENDING
}
}
@Service
@RequiredArgsConstructor
public class PlaceOrderService {
private final OrderRepository orders;
private final ProductClient products; // @CircuitBreaker + timeout 800 ms
private final OrderSagaOrchestrator saga;
@Transactional
public OrderResponse place(String userId, String key, PlaceOrderRequest req) {
return orders.findByIdempotencyKey(key).map(OrderResponse::from).orElseGet(() -> {
List<PricedItem> items = products.price(req.items()); // current prices
Order order = orders.save(Order.pending(userId, key, items, req.addressId()));
saga.start(order); // saga row + outbox command
return OrderResponse.from(order);
});
}
}