A simple inventory management feature β add item, update stock, list items by category.
Spring Boot wires dependencies automatically via constructor injection β no explicit container file needed.
graph TD
APP["ποΈ App<br/>Spring Boot Β· Auto-config Β· Routes"] --> F_INVENTORY["β‘ features/inventory"]
APP --> F_CATALOG["β‘ features/catalog"]
F_INVENTORY --> E_ITEM["π¦ entities/item"]
F_CATALOG --> E_ITEM
E_ITEM --> SHARED["π§ shared<br/>config Β· api response"]
style APP fill:#e1f5fe,stroke:#0288d1
style F_INVENTORY fill:#f3e5f5,stroke:#7b1fa2
style F_CATALOG fill:#f3e5f5,stroke:#7b1fa2
style E_ITEM fill:#e8f5e9,stroke:#388e3c
style SHARED fill:#fff3e0,stroke:#f57c00
src/main/java/com/example/
βββ app/
β βββ Application.java # @SpringBootApplication
βββ features/
β βββ inventory/
β β βββ api/
β β β βββ InventoryController.java # @RestController (thin handler)
β β βββ CreateItemAction.java # @Component
β β βββ UpdateStockAction.java # @Component
β β βββ dto/
β β βββ CreateItemRequest.java
β β βββ ItemResponse.java
β βββ catalog/
β βββ api/
β β βββ CatalogController.java
β βββ ListItemsAction.java # @Component
β βββ dto/
β βββ CategoryItemsResponse.java
βββ entities/
β βββ item/
β βββ Item.java # @Entity
β βββ ItemDal.java # @Repository (Spring Data JPA)
β βββ ItemNotFoundException.java
βββ shared/
βββ api/
βββ ApiResponse.java
Note
Spring Boot scans @Component, @Repository, and @RestController automatically.
Actions are plain @Component classes β no @Service naming to avoid confusion with the service-layer antipattern.
// shared/api/ApiResponse.java
public record ApiResponse<T>(T data) {
public static <T> ApiResponse<T> of(T data) {
return new ApiResponse<>(data);
}
}// entities/item/Item.java
@Entity
@Table(name = "items")
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String category;
private int quantity;
private BigDecimal price;
// constructors, getters, setters
}// entities/item/ItemDal.java
@Repository
public interface ItemDal extends JpaRepository<Item, Long> {
List<Item> findByCategory(String category);
}// entities/item/ItemNotFoundException.java
public class ItemNotFoundException extends RuntimeException {
public ItemNotFoundException(Long id) {
super("Item not found: " + id);
}
}// features/inventory/CreateItemAction.java
@Component
public class CreateItemAction {
private final ItemDal itemDal;
public CreateItemAction(ItemDal itemDal) {
this.itemDal = itemDal;
}
public ItemResponse execute(CreateItemRequest request) {
var item = new Item();
item.setName(request.name());
item.setCategory(request.category());
item.setQuantity(request.quantity());
item.setPrice(request.price());
return ItemResponse.from(itemDal.save(item));
}
}// features/inventory/UpdateStockAction.java
@Component
public class UpdateStockAction {
private final ItemDal itemDal;
public UpdateStockAction(ItemDal itemDal) {
this.itemDal = itemDal;
}
public ItemResponse execute(Long itemId, int delta) {
var item = itemDal.findById(itemId)
.orElseThrow(() -> new ItemNotFoundException(itemId));
item.setQuantity(item.getQuantity() + delta);
return ItemResponse.from(itemDal.save(item));
}
}// features/inventory/api/InventoryController.java
@RestController
@RequestMapping("/api/inventory")
public class InventoryController {
private final CreateItemAction createItem;
private final UpdateStockAction updateStock;
public InventoryController(CreateItemAction createItem, UpdateStockAction updateStock) {
this.createItem = createItem;
this.updateStock = updateStock;
}
@PostMapping
public ResponseEntity<ApiResponse<ItemResponse>> create(@RequestBody CreateItemRequest request) {
return ResponseEntity.status(201).body(ApiResponse.of(createItem.execute(request)));
}
@PatchMapping("/{id}/stock")
public ResponseEntity<ApiResponse<ItemResponse>> updateStock(
@PathVariable Long id, @RequestParam int delta) {
return ResponseEntity.ok(ApiResponse.of(updateStock.execute(id, delta)));
}
}// features/catalog/ListItemsAction.java
@Component
public class ListItemsAction {
private final ItemDal itemDal;
public ListItemsAction(ItemDal itemDal) {
this.itemDal = itemDal;
}
public List<ItemResponse> execute(String category) {
return itemDal.findByCategory(category)
.stream()
.map(ItemResponse::from)
.toList();
}
}// features/catalog/api/CatalogController.java
@RestController
@RequestMapping("/api/catalog")
public class CatalogController {
private final ListItemsAction listItems;
public CatalogController(ListItemsAction listItems) {
this.listItems = listItems;
}
@GetMapping
public ResponseEntity<ApiResponse<List<ItemResponse>>> listByCategory(@RequestParam String category) {
return ResponseEntity.ok(ApiResponse.of(listItems.execute(category)));
}
}// app/Application.java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}Note
Spring Boot discovers and wires all components automatically. The container registration order (Shared β Entities β Features) is handled by Spring's dependency graph β just declare constructor dependencies and it resolves them.
| Without FAA | With FAA |
|---|---|
ItemService with create, updateStock, listByCategory, getById... |
CreateItemAction, UpdateStockAction, ListItemsAction β one class, one job |
Business logic mixed into @Service classes |
Logic in focused actions; DAL is a plain Spring Data interface |
| Monolithic service becomes a merge conflict magnet | Each action is a separate file β no conflicts |
| Testing requires mocking the entire service | Each action tested with only its direct dependency mocked |