Microservices — Practical Demo
Hands-on examples for Microservices. We'll build a minimal e-commerce topology: Order Service and Inventory Service communicating synchronously (REST) and asynchronously (Kafka).
Familiarize yourself with the Microservices concepts — especially database-per-service, service discovery, and the difference between synchronous and async communication.
Scenario: E-Commerce Order Placement
When a customer places an order:
- Order Service receives the request, persists the order, and publishes an
OrderPlacedevent. - Inventory Service consumes the event and reserves stock.
- If inventory reservation fails, a compensating
InventoryFailedevent triggers order cancellation.
Example 1: Order Service — Minimal Spring Boot Setup
@SpringBootApplication
@EnableFeignClients // {3} ← enables Feign clients for sync service calls
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
} // {6}
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public OrderResponse placeOrder(@RequestBody @Valid PlaceOrderRequest request) { // {10}
return orderService.placeOrder(request);
}
@GetMapping("/{id}")
@ResponseStatus(HttpStatus.OK)
public OrderResponse getOrder(@PathVariable Long id) { // {14}
return orderService.getOrder(id);
}
}
public record PlaceOrderRequest(
@NotNull Long userId,
@NotEmpty List<OrderItem> items, // ← Bean Validation on the nested list
@NotBlank String shippingAddress
) { }
public record OrderItem(
@NotNull Long productId,
@Positive int quantity
) { }
Example 2: Order Service — Persisting and Publishing an Event
@Service
@RequiredArgsConstructor
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate;
public OrderResponse placeOrder(PlaceOrderRequest req) {
Order order = orderRepository.save(Order.pending(req)); // {8} ← persist first
kafkaTemplate.send(
"orders.placed", // {12} ← topic name
order.getId().toString(), // ← partition key (order ID)
new OrderPlacedEvent(
UUID.randomUUID(), // {16} ← unique event ID for idempotency
order.getId(),
req.userId(),
req.items()
)
);
return OrderResponse.from(order);
}
}
Persist before publishing. If you publish first and then the DB write fails, the event is in Kafka but the order doesn't exist. Persisting first means Kafka gets the event only when the order is committed.
spring:
application:
name: order-service # ← service name for Eureka registration
datasource:
url: jdbc:postgresql://localhost:5432/orders # ← dedicated DB schema
username: orders_user
password: orders_pass
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
properties:
spring.json.type.mapping: orderPlaced:com.example.order.events.OrderPlacedEvent
server:
port: 8081 # ← each service runs on a different port locally
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
Example 3: Inventory Service — Consuming the Event Idempotently
The Inventory Service listens on the orders.placed Kafka topic and reserves stock.
@Service
@RequiredArgsConstructor
@Slf4j
public class InventoryEventConsumer {
private final InventoryService inventoryService;
private final ProcessedEventRepository processedEventRepository; // {7} ← idempotency store
private final KafkaTemplate<String, Object> kafkaTemplate;
@KafkaListener(topics = "orders.placed", groupId = "inventory-service")
public void onOrderPlaced(OrderPlacedEvent event) {
// Idempotency guard — skip already-processed events
if (processedEventRepository.existsById(event.eventId())) { // {13}
log.info("Duplicate event {}, skipping", event.eventId());
return;
}
try {
inventoryService.reserveStock(event.orderId(), event.items());
processedEventRepository.save(new ProcessedEvent(event.eventId()));
kafkaTemplate.send("inventory.reserved", // {20} ← success event
event.orderId().toString(),
new InventoryReservedEvent(event.eventId(), event.orderId()));
} catch (InsufficientStockException e) {
log.warn("Insufficient stock for order {}", event.orderId());
kafkaTemplate.send("inventory.failed", // ← failure event triggers compensation
event.orderId().toString(),
new InventoryFailedEvent(event.eventId(), event.orderId(), e.getMessage()));
}
}
}
Expected behavior:
- First delivery of
OrderPlacedEvent→ stock reserved →InventoryReservedEventpublished. - Duplicate delivery (Kafka redelivery) → guard fires → no double-reservation.
- Insufficient stock →
InventoryFailedEventpublished → Order Service cancels the order.
Example 4: Order Service — Listening for Compensation
@Service
@RequiredArgsConstructor
public class OrderCompensationConsumer {
private final OrderRepository orderRepository;
@KafkaListener(topics = "inventory.failed", groupId = "order-service-compensation")
public void onInventoryFailed(InventoryFailedEvent event) { // {8}
orderRepository.findById(event.orderId()).ifPresent(order -> {
order.cancel("INVENTORY_UNAVAILABLE"); // {12} ← business status change
orderRepository.save(order);
log.info("Order {} cancelled: {}", event.orderId(), event.reason());
});
}
}
Example 5: Synchronous Service Call with Circuit Breaker
When Order Service needs user details from User Service, it makes a synchronous REST call protected by a circuit breaker:
@Service
@RequiredArgsConstructor
public class UserServiceClient {
private final RestClient restClient; // {5} ← Spring Boot 3.2+ RestClient
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
@Retry(name = "userService")
public UserDto getUser(Long userId) {
return restClient.get()
.uri("http://user-service/api/v1/users/{id}", userId) // ← resolved by service discovery
.retrieve()
.onStatus(status -> status.is4xxClientError(),
(req, res) -> { throw new UserNotFoundException(userId); })
.body(UserDto.class); // {13} ← deserialize response DTO
}
public UserDto getUserFallback(Long userId, Exception ex) {
log.warn("User service unavailable for user {}, using stub", userId);
return UserDto.anonymous(userId); // ← graceful degradation
}
}
Exercises
Try these on your own to solidify understanding:
- Easy: Add a
GET /api/v1/orders/{id}/statusendpoint to Order Service that returns the current order status (PENDING,CONFIRMED,CANCELLED). - Medium: Add a
Notification Servicethat listens forinventory.reservedevents and sends a "Your order is confirmed" email. Ensure the consumer is idempotent. - Hard: Implement a Saga timeout: if an order remains in
PENDINGstate for more than 5 minutes (no Kafka response from Inventory Service), a Spring@Scheduledjob should fire, cancel the order, and publish aSagaTimeoutEvent.
Back to Topic
Return to the Microservices note for theory, interview questions, and further reading.