Skip to main content
ElastiCORE 3.0 is a release candidate: 3.0.261022-rc1 on Maven Central. Projects on the 2.2 line: see the 2.2 documentation.
Version: 3.0

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​

DirectionWhatNamespaceAdapter
InboundOrderUseCase - the door into the applicationinboundPort (fallback service)the controller (generated)
Outbound, persistenceOrderRepositoryPort - owned by the domaindomainRepository (fallback entity)OrderRepositoryAdapter (generated)
Outbound, SQL/HTTPthe port: DSLporta 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:

blueprint/petstore/env.yml
  namespace:
base: io.elasticore.sample.petstore

The states: of PurchaseOrder became a use case, a controller endpoint and a method on the base:

application/port/in/PurchaseOrderUseCase.java (generated)
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);
}
adapter/in/web/PurchaseOrderController.java (generated once)
    @Operation(summary = "PurchaseOrder: ship")
@PostMapping("/{orderId}/ship")
public PurchaseOrderDTO ship(@PathVariable("orderId") String orderId) {
return purchaseOrderService.ship(orderId);
}
domain/model/PurchaseOrderBase.java (regenerated)
	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:

application/PurchaseOrderService.java (yours)
@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​

env.yml - the hexagonal preset (what namespace: base fills in)
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.

Kotlin and the in package

in 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).