Event-driven stock management, HATEOAS, and handling complex inventory logic across microservices.
Part 3 followed an order from HTTP request to RabbitMQ message. The OrderCreated event was left in the queue. This post focuses on how the Inventory Service consumes it, solves complex stock management challenges, and leverages shared commons infrastructure.
The Inventory Service is a major bounded context in SmartOrder. Structurally similar to Order Service — three-module Maven layout, contract-first API, HATEOAS responses — it introduces several patterns to solve enterprise challenges: polyglot persistence, eventual consistency, and reliable event-driven workflows.
Ownership and boundaries
Inventory Service owns the stock state for each item — available, reserved, out of stock, discontinued. It does not manage order lifecycle or pricing. Boundaries are strict:
- No cross-service joins
- No shared schemas
- No synchronous calls
It consumes OrderCreated events and reacts, enabling decoupled, eventually consistent microservices — a cornerstone of SmartOrder architecture.
The domain model and commons integration
@Entity
@Table
@Data
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@Jacksonized
@EntityListeners(AuditingEntityListener.class)
public class Inventory implements it.portus.business.commons.model.Entity<UUID> {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String description;
@NotNull
@Builder.Default
@Enumerated(EnumType.STRING)
private InventoryStatus status = InventoryStatus.PENDING;
@CreatedDate
@Setter(AccessLevel.NONE)
@Column(nullable = false, updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
@Setter(AccessLevel.NONE)
@Column(nullable = false)
private LocalDateTime lastModifiedDate;
}
-
InventoryStatusdrives the saga. -
@CreatedDate/@LastModifiedDateleverage commons-business auditing. - Repository is intentionally thin:
public interface InventoryRepository extends JpaRepository<Inventory, UUID> {}
Polyglot persistence
Inventory Service uses Spring Data JPA with H2/Postgres, while Order Service uses MongoDB. Why?
- Stock levels need atomic updates
- Reservations require transactional semantics
- JPA auditing simplifies commons integration
Commons modules provide base entities, mapper interfaces, and helpers, ensuring consistent patterns across services.
Event-driven consumption
@Bean
public Consumer<OrderCreatedEvent> orderCreatedConsumer() {
return event -> orderConfirmationPublisher.send(event);
}
spring:
cloud:
function:
definition: orderCreatedConsumer
stream:
function:
bindings:
orderCreatedConsumer-in-0: consumeOrderCreated
bindings:
consumeOrderCreated:
destination: pending-orders
group: inventory-group
-
inventory-groupensures single processing per event across instances - Stateless consumer, purely event-driven
- Commons provide shared DTOs and binding constants:
@UtilityClass
public class BindingNames {
public static final String PUBLISH_ORDER_CONFIRMED = "publishOrderConfirmed";
public static final String PUBLISH_ORDER_OUT_OF_STOCK = "publishOrderOutOfStock";
}
Availability check and conditional event publishing
@Slf4j
@Component
@RequiredArgsConstructor
public class OrderConfirmationPublisher {
private final StreamBridge streamBridge;
private final InventoryService inventoryService;
public void send(OrderCreatedEvent event) {
boolean available = inventoryService.checkAvailability(event.getOrderId());
if (available) {
streamBridge.send(BindingNames.PUBLISH_ORDER_CONFIRMED, new OrderConfirmedEvent(event.getOrderId()));
} else {
streamBridge.send(BindingNames.PUBLISH_ORDER_OUT_OF_STOCK,
new OrderOutOfStockEvent(event.getOrderId(), "Insufficient stock"));
}
}
}
- Decision logic is imperative but fully decoupled via events
- Shared events-model from commons ensures consistent serialization
-
StreamBridgeallows dynamic selection of output bindings
HATEOAS in REST responses
@Override
public ResponseEntity<EntityModel<Inventory>> getInventoryById(UUID id) {
return inventoryService.findById(id)
.map(inventoryMapper::toDTO)
.map(hateoasHelper::toEntityModel)
.map(ResponseEntity::ok)
.orElseThrow(() -> new InventoryNotFoundException(id));
}
- HATEOAS links generated via
hateoasHelperguide clients dynamically - Supports patch/update operations without hardcoding URIs
- Commons provide shared mapper interfaces, page mappers, HATEOAS helpers
Event-driven workflow: solved challenges
- Order Service publishes
OrderCreated - Inventory Service consumes it
-
OrderConfirmationPublisherdecides based on stock - Events published:
OrderConfirmedEventorOrderOutOfStockEvent - Order Service updates state
Solved challenges:
- Event-driven stock reservation without distributed transactions
- Atomic availability checks leveraging JPA transactions
- Shared infrastructure reduces boilerplate
TODO: checkAvailability
@Override
public boolean checkAvailability(String orderId) {
// TODO: validate each product item against actual stock
return true;
}
- Placeholder always returns
true -
Next step for developers: integrate product line items from
OrderCreatedEventand validate quantities per stock record - This TODO is not part of this article's scope, but essential for full inventory correctness
Bootstrap and test data (developer hint)
@AutoConfiguration
@ConditionalOnClass(Instancio.class)
public class TestDataLoaderConfiguration {
@Bean
CommandLineRunner loadTestData(InventoryService service) {
return args -> service.saveAll(Instancio.ofList(Inventory.class).size(5).ignoreFields().create());
}
}
- Instancio generates seed data for local development
- Ensures consistent setup without touching production
What's next: commons modules
Focus will be on services/commons:
- Shared business logic, DTOs, event definitions, and helpers
- Sub-modules used across
order-serviceandinventory-service -
Includes:
- Entity base classes
- MapStruct mappers
- HATEOAS helpers
- Test data loaders
Commons are key to scaling SmartOrder without duplicating boilerplate.
Repo is open: → github.com/portus84/smartorder-ms
Top comments (0)