The Inventory Service manages the product catalog and stock levels. It implements stock reservation logic to prevent overselling and maintains product information including SKUs, prices, and availability.
Port: 9090 | Database: MySQL (inventorydb) | Eureka Name: msvc-inventory
The Inventory Service is the most complete implementation of hexagonal architecture in the StreamLine platform. It serves as the reference implementation for the ports and adapters pattern.
The Stock entity manages inventory quantities with sophisticated reservation logic:
package com.microservice.inventory.domain.model;/** * Manages stock with reservation capabilities. * Tracks total quantity and reserved quantity. */public class Stock { private final Long id; private final Long productId; private final int totalQuantity; private final int totalReserved; public Stock(Long id, Long productId, int totalQuantity, int totalReserved) { validateQuantity(totalQuantity); validateReserved(totalReserved, totalQuantity); this.id = id; this.productId = productId; this.totalQuantity = totalQuantity; this.totalReserved = totalReserved; } /** * Reserve stock for an order. * Throws exception if insufficient available stock. */ public Stock reserveStock(int amount) { if (amount > available()) { throw new DomainException( "No hay suficiente stock disponible para reservar. " + "Stock disponible: " + available()); } validateReserved(amount, totalQuantity); return new Stock(this.id, this.productId, this.totalQuantity, this.totalReserved + amount); } /** * Confirm reservation and reduce total stock. * Used when order is shipped. */ public Stock confirmReservation(int amount) { if (amount > totalReserved) { throw new DomainException( "No hay suficiente stock reservado para confirmar"); } return new Stock(this.id, this.productId, this.totalQuantity - amount, this.totalReserved - amount); } /** * Release reserved stock (e.g., cancelled order). */ public Stock releaseStock(int amount) { if (amount > totalReserved) { throw new DomainException( "No hay suficiente stock reservado para liberar"); } return new Stock(this.id, this.productId, this.totalQuantity, this.totalReserved - amount); } /** * Add new stock to inventory. */ public Stock addStock(int amount) { validateQuantity(amount); return new Stock(this.id, this.productId, this.totalQuantity + amount, this.totalReserved); } // Calculate available stock public int available() { return totalQuantity - totalReserved; } private void validateQuantity(int quantity) { if (quantity < 0) { throw new DomainException( "La cantidad no puede ser negativa"); } } private void validateReserved(int reserved, int total) { if (reserved < 0) { throw new DomainException( "La cantidad reservada no puede ser negativa"); } if (reserved > total) { throw new DomainException( "La cantidad reservada no puede ser mayor que la total"); } }}
Stock Reservation Pattern: The Stock entity prevents overselling by tracking reserved quantities separately from total stock. Available stock = total - reserved.
package com.microservice.inventory.domain.valueobject;/** * Represents the SKU (Stock Keeping Unit) of a product. * Immutable value object with validation. */public record Sku(String value) { public Sku { if (value == null || value.isBlank()) { throw new DomainException( "El SKU es obligatorio"); } }}
Price Value Object
package com.microservice.inventory.domain.valueobject;import java.math.BigDecimal;/** * Represents a product price with validation. * Enforces positive values and 2 decimal places. */public record Price(BigDecimal amount) { public Price { if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { throw new DomainException( "El precio no puede ser nulo o 0"); } if (amount.scale() > 2) { throw new DomainException( "El precio no puede tener más de 2 decimales"); } }}
Value objects encapsulate validation logic and business rules, making invalid states impossible.
package com.microservice.inventory.application.port.input;import com.microservice.inventory.domain.model.Product;import com.microservice.inventory.domain.model.ProductDetails;import com.microservice.inventory.domain.model.Stock;/** * Use case for creating products. * This is an INPUT PORT (interface) in hexagonal architecture. */public interface CreateProductUseCase { /** * Creates a new product with initial stock. * @return Product details including generated ID */ ProductDetails createProduct(Product product, Stock stock);}
The repository adapter implements the output port using JPA:
package com.microservice.inventory.infrastructure.adapter.output.persistence.Product;import org.springframework.stereotype.Component;import com.microservice.inventory.application.port.output.ProductRepository;import com.microservice.inventory.domain.model.Product;/** * JPA implementation of ProductRepository port. * Translates between domain model and JPA entities. */@Componentpublic class ProductRepositoryAdapter implements ProductRepository { private final JpaProductRepository jpaProductRepository; private final ProductEntityMapper productEntityMapper; public ProductRepositoryAdapter( JpaProductRepository jpaProductRepository, ProductEntityMapper productEntityMapper) { this.jpaProductRepository = jpaProductRepository; this.productEntityMapper = productEntityMapper; } @Override public Optional<Product> findById(Long id) { return jpaProductRepository.findById(id) .map(productEntityMapper::toDomain); } @Override public Optional<Product> findBySku(String sku) { return jpaProductRepository.findBySku(sku) .map(productEntityMapper::toDomain); } @Override public Product save(Product productModel) { ProductEntity newProductEntity = ProductEntity.builder() .sku(productModel.getSku().value()) .name(productModel.getName()) .description(productModel.getDescription()) .price(productModel.getPrice().amount()) .build(); ProductEntity savedEntity = jpaProductRepository.save(newProductEntity); return productEntityMapper.toDomain(savedEntity); } @Override public List<Product> findAll() { List<ProductEntity> productEntities = jpaProductRepository.findAll(); return productEntityMapper.toDomainList(productEntities); }}
Layer Separation: Notice how the adapter translates between ProductEntity (infrastructure concern) and Product (domain model). The domain layer never knows about JPA or databases.