DEV Community

Cover image for SmartOrder — Part 4: Inside the Inventory Service
Francesco Portus
Francesco Portus

Posted on

SmartOrder — Part 4: Inside the Inventory Service

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.

👉 services/inventory-service


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;
}
Enter fullscreen mode Exit fullscreen mode
  • InventoryStatus drives the saga.
  • @CreatedDate/@LastModifiedDate leverage commons-business auditing.
  • Repository is intentionally thin:
public interface InventoryRepository extends JpaRepository<Inventory, UUID> {}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
spring:
  cloud:
    function:
      definition: orderCreatedConsumer
    stream:
      function:
        bindings:
          orderCreatedConsumer-in-0: consumeOrderCreated
      bindings:
        consumeOrderCreated:
          destination: pending-orders
          group: inventory-group
Enter fullscreen mode Exit fullscreen mode
  • inventory-group ensures 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";
}
Enter fullscreen mode Exit fullscreen mode

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"));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Decision logic is imperative but fully decoupled via events
  • Shared events-model from commons ensures consistent serialization
  • StreamBridge allows 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));
}
Enter fullscreen mode Exit fullscreen mode
  • HATEOAS links generated via hateoasHelper guide clients dynamically
  • Supports patch/update operations without hardcoding URIs
  • Commons provide shared mapper interfaces, page mappers, HATEOAS helpers

Event-driven workflow: solved challenges

  1. Order Service publishes OrderCreated
  2. Inventory Service consumes it
  3. OrderConfirmationPublisher decides based on stock
  4. Events published: OrderConfirmedEvent or OrderOutOfStockEvent
  5. 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;
}
Enter fullscreen mode Exit fullscreen mode
  • Placeholder always returns true
  • Next step for developers: integrate product line items from OrderCreatedEvent and 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());
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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-service and inventory-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)