Multi-slice example for 10β30 entities¶
When your service grows beyond a handful of entities, folder salad looms. After all,
Once you pass five entities, a flat βone-entity demoβ turns into a Whereβs-Waldo. The fix is to group by feature/bounded context (vertical slices), and keep the same edges β core rhythm inside each slice. For an inventory system, four slices keep you sane:
- catalog/ β products, categories, brands
- inventory/ β warehouses, stock levels, movements
- sales/ β customers, orders, order items
- procurement/ β suppliers, purchase orders
Hereβs a paste-ready skeleton you can drop into src/. It scales without devolving into folder salad.
inventory-service/
ββ build.gradle / pom.xml
ββ README.md
ββ .editorconfig
ββ .gitignore
ββ src
ββ main
β ββ java/com/edge/inventory
β β ββ web/ # HTTP edge: controllers, DTOs, ProblemDetail
β β β ββ catalog/
β β β β ββ ProductController.java
β β β β ββ CategoryController.java
β β β β ββ dto/
β β β β β ββ CreateProductRequest.java
β β β β β ββ UpdateProductRequest.java
β β β β β ββ ProductResponse.java
β β β β β ββ CreateCategoryRequest.java
β β β β β ββ CategoryResponse.java
β β β β ββ mapper/
β β β β ββ ProductWebMapper.java
β β β β ββ CategoryWebMapper.java
β β β ββ inventory/
β β β β ββ WarehouseController.java
β β β β ββ StockItemController.java # /warehouses/{id}/stock
β β β β ββ dto/
β β β β β ββ CreateWarehouseRequest.java
β β β β β ββ WarehouseResponse.java
β β β β β ββ AdjustStockRequest.java
β β β β β ββ StockItemResponse.java
β β β β ββ mapper/
β β β β ββ WarehouseWebMapper.java
β β β β ββ StockWebMapper.java
β β β ββ sales/
β β β β ββ OrderController.java
β β β β ββ CustomerController.java
β β β β ββ dto/
β β β β β ββ CreateOrderRequest.java # contains line items
β β β β β ββ OrderResponse.java
β β β β β ββ CreateCustomerRequest.java
β β β β β ββ CustomerResponse.java
β β β β ββ mapper/
β β β β ββ OrderWebMapper.java
β β β β ββ CustomerWebMapper.java
β β β ββ procurement/
β β β β ββ SupplierController.java
β β β β ββ PurchaseOrderController.java
β β β β ββ dto/
β β β β β ββ CreateSupplierRequest.java
β β β β β ββ SupplierResponse.java
β β β β β ββ CreatePurchaseOrderRequest.java
β β β β β ββ PurchaseOrderResponse.java
β β β β ββ mapper/
β β β β ββ SupplierWebMapper.java
β β β β ββ PurchaseOrderWebMapper.java
β β β ββ error/
β β β β ββ GlobalExceptionHandler.java # @RestControllerAdvice β ProblemDetail
β β β β ββ ProblemTypes.java # stable type URIs / optional codes
β β β ββ config/
β β β ββ WebConfig.java
β β ββ app/ # Use-cases: orchestrate domain + repos, @Transactional
β β β ββ catalog/
β β β β ββ CreateProductUseCase.java
β β β β ββ UpdateProductUseCase.java
β β β β ββ ListProductsUseCase.java
β β β β ββ CreateCategoryUseCase.java
β β β β ββ CatalogService.java # implements multiple use-case interfaces
β β β ββ inventory/
β β β β ββ CreateWarehouseUseCase.java
β β β β ββ AdjustStockUseCase.java
β β β β ββ TransferStockUseCase.java
β β β β ββ InventoryService.java
β β β ββ sales/
β β β β ββ CreateOrderUseCase.java
β β β β ββ GetOrderUseCase.java
β β β β ββ CreateCustomerUseCase.java
β β β β ββ SalesService.java
β β β ββ procurement/
β β β β ββ CreateSupplierUseCase.java
β β β β ββ CreatePurchaseOrderUseCase.java
β β β β ββ ProcurementService.java
β β β ββ exception/
β β β ββ ProductNotFoundException.java
β β β ββ CategoryNotFoundException.java
β β β ββ WarehouseNotFoundException.java
β β β ββ InsufficientStockException.java
β β β ββ OrderNotFoundException.java
β β β ββ SupplierNotFoundException.java
β β ββ domain/ # Entities + repository ports (interfaces)
β β β ββ catalog/
β β β β ββ Product.java
β β β β ββ Category.java
β β β β ββ Brand.java
β β β β ββ ProductRepository.java
β β β β ββ CategoryRepository.java
β β β ββ inventory/
β β β β ββ Warehouse.java
β β β β ββ StockItem.java # product_id + warehouse_id + qty
β β β β ββ StockMovement.java # audit of adjustments/transfers
β β β β ββ WarehouseRepository.java
β β β β ββ StockRepository.java
β β β ββ sales/
β β β β ββ Customer.java
β β β β ββ Order.java
β β β β ββ OrderItem.java
β β β β ββ OrderRepository.java
β β β β ββ CustomerRepository.java
β β β ββ procurement/
β β β β ββ Supplier.java
β β β β ββ PurchaseOrder.java
β β β β ββ PurchaseOrderItem.java
β β β β ββ PurchaseOrderRepository.java
β β β ββ common/
β β β ββ BaseEntity.java
β β β ββ Money.java # value object (amount + currency)
β β β ββ Quantity.java # value object for stock quantities
β β ββ infra/ # Adapters/config
β β ββ persistence/
β β β ββ JpaConfig.java
β β β ββ SpecBuilders.java # optional: Spring Data Specifications
β β ββ mapping/
β β β ββ MapStructConfig.java
β β ββ security/
β β ββ SecurityConfig.java # optional later
β ββ resources
β ββ application.yml
β ββ application-local.yml
β ββ db/migration/
β β ββ V1__init_catalog.sql
β β ββ V2__init_inventory.sql
β β ββ V3__init_sales.sql
β β ββ V4__init_procurement.sql
β ββ logback-spring.xml
ββ test
ββ java/com/edge/inventory
β ββ web/catalog/
β β ββ ProductControllerIT.java
β β ββ CategoryControllerIT.java
β ββ web/inventory/
β β ββ WarehouseControllerIT.java
β ββ app/catalog/
β β ββ CatalogServiceTest.java
β ββ app/inventory/
β β ββ InventoryServiceTest.java
β ββ domain/catalog/
β β ββ ProductRepositoryIT.java
β ββ domain/inventory/
β ββ StockRepositoryIT.java
ββ resources/
ββ application-test.yml
Why this stays navigable with 10β30 entities¶
- Vertical slices: You always know where to look. All βproductβ stuff is under
catalog/product*across layers. - Repeatable pattern: Each slice has
web/app/domainsubfolders with parallel names. Muscle memory beats search. - Shared core:
domain/commonholds value objects and base types used cross-slice, not a dumping ground.
Relationships that wonβt tangle your brain¶
ProductβCategory(many-to-one).StockItemtiesProduct+Warehousewithquantity.OrderhasOrderItems (each referencesProductand a snapshot price).PurchaseOrdermirrorsOrderbut withSupplierand incoming quantities.- Keep aggregates small:
OrderownsOrderItems;Productdoesnβt ownStockItemrows (inventory slice does).
MapStruct placement rule (unchanged)¶
- Web mappers live at the edge (
web/.../mapper), convertingRequest β CommandandEntity β Response. - If you add persistence projections/specs, put those mappers in
infra/mapping/.
// infra/mapping/MapStructConfig.java
@Configuration
@MapperConfig(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE
)
public class MapStructConfig {}
Then annotate each mapper: @Mapper(config = MapStructConfig.class).
Use-case seams (how services donβt balloon)¶
- One service per slice is fine early:
CatalogService,InventoryService, etc., implement multiple use-case interfaces. - When a service grows obese, split by workflow: e.g.,
OrderingServicevsOrderQueryService. - Keep commands/inputs narrow:
CreateOrderRequestincludes line items;AdjustStockRequestis just product+warehouse+delta.
ProblemDetail catalog (so errors stay boring)¶
- Keep
GlobalExceptionHandlerinweb/error/. -
Put stable
typeURIs inProblemTypeslike: -
/problems/catalog/product-not-found /problems/inventory/insufficient-stock- Base URI (
problems.base) inapplication.yml, read as@ConfigurationPropertiesso you donβt hardcode strings.
Flyway starter pack (one file per slice)¶
V1__init_catalog.sql
CREATE TABLE category(
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE product(
id BIGSERIAL PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
category_id BIGINT NOT NULL REFERENCES category(id)
);
V2__init_inventory.sql
CREATE TABLE warehouse(
id BIGSERIAL PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL
);
CREATE TABLE stock_item(
product_id BIGINT NOT NULL REFERENCES product(id),
warehouse_id BIGINT NOT NULL REFERENCES warehouse(id),
quantity NUMERIC(19,2) NOT NULL DEFAULT 0,
PRIMARY KEY(product_id, warehouse_id)
);
CREATE TABLE stock_movement(
id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES product(id),
warehouse_id BIGINT NOT NULL REFERENCES warehouse(id),
delta NUMERIC(19,2) NOT NULL,
reason TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
V3__init_sales.sql
CREATE TABLE customer(
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL
);
CREATE TABLE "order"(
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customer(id),
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE order_item(
order_id BIGINT NOT NULL REFERENCES "order"(id),
line_no INT NOT NULL,
product_id BIGINT NOT NULL REFERENCES product(id),
qty NUMERIC(19,2) NOT NULL,
unit_price NUMERIC(19,2) NOT NULL,
PRIMARY KEY(order_id, line_no)
);
V4__init_procurement.sql
CREATE TABLE supplier(
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
email TEXT
);
CREATE TABLE purchase_order(
id BIGSERIAL PRIMARY KEY,
supplier_id BIGINT NOT NULL REFERENCES supplier(id),
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE purchase_order_item(
purchase_order_id BIGINT NOT NULL REFERENCES purchase_order(id),
line_no INT NOT NULL,
product_id BIGINT NOT NULL REFERENCES product(id),
qty NUMERIC(19,2) NOT NULL,
unit_cost NUMERIC(19,2) NOT NULL,
PRIMARY KEY(purchase_order_id, line_no)
);
Testing layout that mirrors slices¶
- Controller IT per slice (
web/catalog/*IT.java) assertingLocation, JSON shape, and ProblemDetails. - Service tests per slice for business rules (stock never negative, order totals).
- Repo slice tests (
@DataJpaTest) for constraints (UNIQUE, FKs) and query specs.
Navigation aids so you never get lost¶
- Add
package-info.javain each slice (web/catalog,app/inventory, β¦) with a one-paragraph doc of what lives there. - Put a tiny
README.mdin each top slice folder explaining flows and key use-cases. - Keep DTO names scenario-first (
AdjustStockRequest,TransferStockRequest) rather than generic (UpdateWarehouse).