package com.foodtech.kitchen.domain.model;public enum ProductType { DRINK(Station.BAR), HOT_DISH(Station.HOT_KITCHEN), COLD_DISH(Station.COLD_KITCHEN); private final Station station; ProductType(Station station) { this.station = station; } public Station getStation() { return station; }}
Design Decision: Why ProductType knows its Station?
Benefits of this approach:✅ Open/Closed Principle: Adding new product types doesn’t require modifying mapping logic elsewhere✅ Single Source of Truth: The relationship between product type and station is defined in one place✅ Type Safety: Enum ensures only valid stationsAlternative (rejected):
// ❌ Violates OCP - requires modification when adding typespublic Station mapProductToStation(ProductType type) { return switch(type) { case DRINK -> Station.BAR; case HOT_DISH -> Station.HOT_KITCHEN; case COLD_DISH -> Station.COLD_KITCHEN; // Must add new cases here };}
public class Task { private final Long id; private final Long orderId; private final Station station; private final String tableNumber; private final List<Product> products; private final LocalDateTime createdAt; private TaskStatus status; private LocalDateTime startedAt; private LocalDateTime completedAt; // Constructor for NEW tasks (no ID yet) public Task(Long orderId, Station station, String tableNumber, List<Product> products, LocalDateTime createdAt) { validate(orderId, station, tableNumber, products, createdAt); this.id = null; this.orderId = orderId; this.station = station; this.tableNumber = tableNumber; this.products = new ArrayList<>(products); // Defensive copy this.createdAt = createdAt; this.status = TaskStatus.PENDING; } // Lifecycle methods public void start() { if (this.status != TaskStatus.PENDING) { throw new IllegalStateException( "Task must be in PENDING status to start" ); } this.status = TaskStatus.IN_PREPARATION; this.startedAt = LocalDateTime.now(); } public void complete() { if (this.status != TaskStatus.IN_PREPARATION) { throw new IllegalStateException( "Task must be in IN_PREPARATION status to complete" ); } this.status = TaskStatus.COMPLETED; this.completedAt = LocalDateTime.now(); }}
Rich Domain Model: The Task entity enforces its own business rules. You cannot complete a task that hasn’t been started.
// ✅ Use case depends on abstract repository (Application → Application)public class ProcessOrderUseCase { private final OrderRepository repository; // Interface from application layer}// ✅ Adapter implements the interface (Infrastructure → Application)@Componentpublic class OrderRepositoryAdapter implements OrderRepository { // Implementation details}
// ❌ Use case depends on concrete adapter (Application → Infrastructure)public class ProcessOrderUseCase { private final OrderRepositoryAdapter adapter; // Concrete class!}// ❌ Domain depends on Spring Framework (Domain → Infrastructure)public class TaskDecomposer { @Autowired // ❌ Spring annotation in domain! private OrderValidator validator;}// ❌ Domain depends on JPA (Domain → Infrastructure)@Entity // ❌ JPA annotation in domain!public class Order { @Id private Long id;}
// BAD - Entity is just a data bagpublic class Task { private TaskStatus status; public TaskStatus getStatus() { return status; } public void setStatus(TaskStatus status) { this.status = status; }}// Service does all the workpublic class TaskService { public void startTask(Task task) { if (task.getStatus() != TaskStatus.PENDING) { throw new IllegalStateException("Cannot start"); } task.setStatus(TaskStatus.IN_PREPARATION); }}// GOOD - Entity has behaviorpublic class Task { private TaskStatus status; public void start() { if (this.status != TaskStatus.PENDING) { throw new IllegalStateException("Cannot start"); } this.status = TaskStatus.IN_PREPARATION; this.startedAt = LocalDateTime.now(); }}
❌ Layer Leakage
// BAD - Controller logic in use casepublic class ProcessOrderUseCase { public ResponseEntity<OrderDTO> execute(OrderDTO dto) { // ❌ HTTP concern in application layer! return ResponseEntity.ok(dto); }}// BAD - JPA in domain@Entity // ❌ Infrastructure annotation!public class Order { // Domain polluted with JPA}// GOOD - Clean separationpublic class ProcessOrderUseCase { public List<Task> execute(Order order) { // Pure domain objects }}