| name | hexagonal-architecture |
| description | Use when the project follows hexagonal (ports & adapters) architecture. Prevents domain code from depending on Spring or JPA. Use when you see packages like domain/, application/, infrastructure/, or adapters/ in the project structure.
|
Hexagonal Architecture
Package Structure
src/main/java/com/example/
├── domain/ ← Pure Java. Zero framework dependencies.
│ ├── model/ ← Entities, value objects, aggregates
│ ├── port/
│ │ ├── in/ ← Use case interfaces (driving ports)
│ │ └── out/ ← Repository/external interfaces (driven ports)
│ └── service/ ← Domain services (pure business logic)
├── application/ ← Orchestrates use cases. Spring allowed here.
│ └── usecase/ ← @Service implementations of domain ports
└── infrastructure/ ← All framework/DB/HTTP details
├── persistence/ ← JPA adapters implementing out ports
├── web/ ← REST controllers (driving adapters)
└── external/ ← HTTP clients, messaging adapters
Domain Layer — Zero Spring
public class Order {
private final OrderId id;
private final CustomerId customerId;
private OrderStatus status;
private final List<OrderItem> items;
private Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
this.status = OrderStatus.PENDING;
this.items = new ArrayList<>();
}
public static Order create(CustomerId customerId) {
return new Order(OrderId.generate(), customerId);
}
public void addItem(ProductId productId, int quantity, Money price) {
if (status != OrderStatus.PENDING)
throw new OrderNotModifiableException(id);
items.add(new OrderItem(productId, quantity, price));
}
}
public record OrderId(UUID value) {
public static OrderId generate() { return new OrderId(UUID.randomUUID()); }
public static OrderId of(String value) { return new OrderId(UUID.fromString(value)); }
}
Ports — Interfaces Only
public interface CreateOrderUseCase {
Order createOrder(CreateOrderCommand command);
}
public record CreateOrderCommand(CustomerId customerId, List<OrderItemData> items) {}
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(OrderId id);
List<Order> findByCustomer(CustomerId customerId);
}
public interface InventoryPort {
void reserve(List<OrderItem> items);
void release(List<OrderItem> items);
}
Application Layer — Use Case Implementation
@Service
@RequiredArgsConstructor
@Transactional
public class CreateOrderService implements CreateOrderUseCase {
private final OrderRepository orderRepository;
private final InventoryPort inventoryPort;
@Override
public Order createOrder(CreateOrderCommand command) {
Order order = Order.create(command.customerId());
command.items().forEach(item ->
order.addItem(item.productId(), item.quantity(), item.price()));
inventoryPort.reserve(order.getItems());
return orderRepository.save(order);
}
}
Infrastructure — Adapters
@Repository
@RequiredArgsConstructor
public class JpaOrderRepository implements OrderRepository {
private final SpringDataOrderRepository springDataRepo;
private final OrderMapper mapper;
@Override
public Order save(Order order) {
OrderJpaEntity entity = mapper.toEntity(order);
return mapper.toDomain(springDataRepo.save(entity));
}
@Override
public Optional<Order> findById(OrderId id) {
return springDataRepo.findById(id.value()).map(mapper::toDomain);
}
}
interface SpringDataOrderRepository extends JpaRepository<OrderJpaEntity, UUID> {}
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
public class OrderController {
private final CreateOrderUseCase createOrderUseCase;
@PostMapping
public ResponseEntity<ApiResponse<OrderResponse>> create(@Valid @RequestBody CreateOrderRequest request) {
Order order = createOrderUseCase.createOrder(request.toCommand());
return ResponseEntity.status(201).body(ApiResponse.ok(OrderResponse.from(order)));
}
}
Gotchas
- Agent imports
javax.persistence in domain classes — domain must be framework-free
- Agent injects
JpaRepository directly into use cases — use domain port interfaces
- Agent puts
@Transactional on domain services — belongs in application layer
- Agent mixes driving and driven ports —
port/in = what app offers, port/out = what app needs
- Agent creates anemic domain with only getters/setters — behavior belongs on domain objects