Hexagonal (hexagonal-springboot)
architecture = 'hexagonal' is the DDD reading plus the inbound side of the hexagon:
every exposed aggregate root gets a use-case interface, the application service implements it, and
the controller injects the interface instead of the class.
application/port/in/OrderUseCase <- the application's surface, called from outside
application/OrderService <- implements OrderUseCase (your extension service)
adapter/in/web/OrderController <- injects OrderUseCase, not the class
The port's methods are the generated service's surface, so "the service implements the port" is
checked by the compiler on every build. Methods removed with @restExclude(...) are left out of the
use case too.
Both directions
| Direction | What | Namespace | Adapter |
|---|---|---|---|
| Inbound | OrderUseCase - the door into the application | inboundPort (fallback service) | the controller (generated) |
| Outbound, persistence | OrderRepositoryPort - owned by the domain | domainRepository (fallback entity) | OrderRepositoryAdapter (generated) |
| Outbound, SQL/HTTP | the port: DSL | port | a runtime proxy (nothing generated) |
From the pet store
The pet store sample is hexagonal Java on Spring Boot 4. Its env.yml names one base package:
namespace:
base: io.elasticore.sample.petstore
The states: of PurchaseOrder became a use case, a controller endpoint and a method on the base:
public interface PurchaseOrderUseCase {
PurchaseOrderDTO save(PurchaseOrderDTO dto);
Optional<PurchaseOrderDTO> findById(String orderId);
Page<PurchaseOrderDTO> findBySearch(PurchaseOrderSrchDTO searchDTO);
List<Boolean> delete(List<PurchaseOrderKeyDTO> dtoList);
boolean deleteById(String orderId);
PurchaseOrderDTO ship(String orderId);
PurchaseOrderDTO cancel(String orderId);
}
@Operation(summary = "PurchaseOrder: ship")
@PostMapping("/{orderId}/ship")
public PurchaseOrderDTO ship(@PathVariable("orderId") String orderId) {
return purchaseOrderService.ship(orderId);
}
public void ship() {
moveStatus("ship", OrderStatus.SHIPPED, OrderStatus.PLACED);
raiseShipped();
}
/** Whether ship may be given now: the status is PLACED. */
public boolean canShip() {
return this.status == OrderStatus.PLACED;
}
PurchaseOrder has no @editable field, so its use case has no update - an order changes by its
commands only.
The extension service is yours and implements the use case; its constructor takes the two ports and
the EntityManager:
@Service
public class PurchaseOrderService extends PurchaseOrderCoreService
implements io.elasticore.sample.petstore.application.port.in.PurchaseOrderUseCase {
public PurchaseOrderService(PurchaseOrderRepositoryPort repository,
io.elasticore.sample.petstore.application.port.out.PurchaseOrderQueryPort query,
jakarta.persistence.EntityManager entityManager,
CustomerRepositoryPort customers, /* ... */) {
super(repository, query, entityManager);
// ...
}
}
Package layout
namespace:
entity: com.acme.shop.domain.model
enumeration: com.acme.shop.domain.model
event: com.acme.shop.domain.event
domainService: com.acme.shop.domain.service
domainRepository: com.acme.shop.domain.repository # <Root>RepositoryPort
repository: com.acme.shop.infrastructure.persistence # Spring Data
persistence: com.acme.shop.infrastructure.persistence # the adapter of both ports
service: com.acme.shop.application
dto: com.acme.shop.application.dto
inboundPort: com.acme.shop.application.port.in # <Root>UseCase
queryPort: com.acme.shop.application.port.out # <Root>QueryPort
port: com.acme.shop.application.port.out # port: DSL interfaces
control: com.acme.shop.adapter.in.web
port: interfaces belong to the application: their contracts are written with application DTOs. The
only generated file of an outbound SQL/HTTP port is its interface - the adapter is a runtime proxy.
in packagein in adapter.in.web is a Kotlin keyword. In Kotlin output the generator writes it as
`in` in packages, imports and qualified names; a Kotlin file of your own writes
package com.acme.adapter.`in`.web too.
When to choose it
The default for a new project - the pet store, the README's quick start and the agent kit's recipes
all use it. Choose ddd instead when you do not want the use-case interfaces.
Samples: elasticore-sample-petstore
(Java, Spring Boot 4), elasticore-template-ddd
(Java, architecture = 'hexagonal'), the shop domain of
elasticore-template-kt
(hexagonal-springboot-kt).