| name | architecture |
| description | Hexagonal architecture, DDD, CQRS, and event sourcing for Java Spring applications. Use when designing package structure, creating Aggregate Roots or Value Objects, implementing ports and adapters, setting up CQRS command/query separation, wiring domain events, writing ArchUnit tests, or creating solution architecture documents and ADRs.
|
| triggers | {"natural":["hexagonal","cqrs","domain event","package structure","ports and adapters"],"code":["UseCase","Port","Adapter","DomainEvent"]} |
| applicability | {"always":false,"triggers":{"files_match":["**/domain/**/*.java","**/application/**/*.java","**/infrastructure/**/*.java","**/interfaces/**/*.java","docs/architecture/**","docs/adr/**"],"code_patterns":["UseCase","Aggregate","DomainEvent","Port","Adapter","BoundedContext"],"task_keywords":["architecture","hexagonal","DDD","CQRS","event sourcing","bounded context","ADR","package structure"],"related_rules":["rules/common/patterns.md","rules/common/spec-driven.md"]}} |
| relevance_assessment | HIGH 90%+: new bounded context, new layer, architectural reshape
HIGH 80%+: cross-layer refactor (e.g., add Port + Adapter pair)
MEDIUM 40-79%: in-layer refactor that respects existing boundaries
LOW 1-39%: small feature addition within existing structure
ZERO: trivial fix, single-method change
|
Architecture — Hexagonal + Solution Design
Hexagonal Layers
Infrastructure → Application → Domain (dependencies inward only)
| Layer | Can Depend On | Cannot Depend On |
|---|
| Domain | Java stdlib only | Spring, JPA, R2DBC, Jackson |
| Application | Domain | Infrastructure |
| Infrastructure | Application, Domain | — |
Package Structure
com.example.order/
├── domain/ # Aggregates, value objects, events, exceptions, repository interfaces
├── application/ # Use cases (input ports), output port interfaces, services
│ └── port/in/ # CreateOrderUseCase
│ └── port/out/ # OrderPersistencePort, PaymentPort
└── infrastructure/ # REST controllers, DB adapters, Kafka, config
└── adapter/in/ # OrderController (depends on port, not service)
└── adapter/out/ # OrderPersistenceAdapter, PaymentApiAdapter
Domain Layer Essentials
Aggregate Root Pattern
Rich model. No setters — state changes through business methods that register domain events.
public class Order {
private final OrderId id;
private OrderStatus status;
private final List<DomainEvent> domainEvents = new ArrayList<>();
public static Order create(CustomerId customerId, List<OrderLine> lines) {
Preconditions.requireNonEmpty(lines, "Order must have at least one line");
var order = new Order(OrderId.generate(), customerId, lines, OrderStatus.CREATED);
order.registerEvent(new OrderCreatedEvent(order.id, customerId, lines));
return order;
}
public static Order reconstitute(OrderId id, CustomerId customerId,
List<OrderLine> lines, OrderStatus status) {
return new Order(id, customerId, lines, status);
}
public void confirm() {
if (this.status != OrderStatus.CREATED) {
throw new OrderAlreadyConfirmedException(this.id);
}
this.status = OrderStatus.CONFIRMED;
registerEvent(new OrderConfirmedEvent(this.id));
}
private void registerEvent(DomainEvent event) {
this.domainEvents.add(event);
}
public List<DomainEvent> domainEvents() {
return Collections.unmodifiableList(domainEvents);
}
public void clearEvents() {
domainEvents.clear();
}
}
create() validates invariants + emits creation event. reconstitute() trusts DB data. Business methods guard state transitions. Application layer dispatches events.
Value Objects
Records with compact constructor validation. Example:
public record Money(BigDecimal amount, String currency) {
public Money {
Objects.requireNonNull(amount, "amount required");
Objects.requireNonNull(currency, "currency required");
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("amount must be non-negative");
}
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) throw new CurrencyMismatchException();
return new Money(this.amount.add(other.amount), this.currency);
}
}
Typed IDs
public record OrderId(String value) {
public OrderId { Objects.requireNonNull(value); }
public static OrderId generate() { return new OrderId(UUID.randomUUID().toString()); }
public static OrderId of(String value) { return new OrderId(value); }
}
CQRS — Command/Query Separation
Command Side (Write Path)
Controller → Command DTO → CommandHandler (UseCase) → Aggregate → Repository Port → DB
↓
DomainEvent → EventPublisher → Kafka/EventStore
public record CreateOrderCommand(String customerId, List<OrderLineDto> lines) {}
public interface CreateOrderUseCase {
Mono<OrderId> execute(CreateOrderCommand command);
}
@RequiredArgsConstructor
public class CreateOrderService implements CreateOrderUseCase {
private final OrderPersistencePort orderPort;
private final DomainEventPublisher eventPublisher;
@Override
public Mono<OrderId> execute(CreateOrderCommand command) {
var order = Order.create(
CustomerId.of(command.customerId()),
command.lines().stream().map(OrderLineMapper::toDomain).toList()
);
return orderPort.save(order)
.doOnSuccess(saved -> eventPublisher.publishAll(saved.domainEvents()))
.map(Order::id);
}
}
Query Side (Read Path)
Controller → Query DTO → QueryHandler → ReadModel Repository → Read DB/View
public interface GetOrderQuery {
Mono<OrderDetailResponse> execute(String orderId);
}
public record OrderDetailResponse(
String orderId, String customerName, String status,
BigDecimal totalAmount, LocalDateTime createdAt
) {}
Command handlers use domain aggregates. Query handlers use flat read models directly. Never mix command and query. Query side can bypass domain layer.
Domain Event Publisher
public interface DomainEventPublisher {
void publishAll(List<DomainEvent> events);
}
@RequiredArgsConstructor
public class KafkaDomainEventPublisher implements DomainEventPublisher {
private final KafkaTemplate<String, DomainEvent> kafka;
@Override
public void publishAll(List<DomainEvent> events) {
events.forEach(event -> kafka.send(event.topic(), event.aggregateId(), event));
}
}
Ports & Adapters Wiring
Port Definitions (Application Layer)
public interface CreateOrderUseCase {
Mono<OrderId> execute(CreateOrderCommand command);
}
public interface OrderPersistencePort {
Mono<Order> save(Order order);
Mono<Order> findById(OrderId id);
}
public interface PaymentPort {
Mono<PaymentResult> charge(OrderId orderId, Money amount);
}
Adapter Implementations (Infrastructure Layer)
@RestController
@RequiredArgsConstructor
public class OrderController {
private final CreateOrderUseCase createOrder;
@PostMapping("/orders")
public Mono<ResponseEntity<OrderId>> create(@Valid @RequestBody CreateOrderRequest request) {
return createOrder.execute(OrderMapper.toCommand(request))
.map(id -> ResponseEntity.created(URI.create("/orders/" + id.value())).body(id));
}
}
@RequiredArgsConstructor
public class OrderPersistenceAdapter implements OrderPersistencePort {
private final OrderR2dbcRepository repository;
private final OrderPersistenceMapper mapper;
@Override
public Mono<Order> save(Order order) {
return repository.save(mapper.toEntity(order)).map(mapper::toDomain);
}
}
Spring Wiring (Configuration)
@Configuration
public class OrderBeanConfig {
@Bean
public CreateOrderUseCase createOrderUseCase(
OrderPersistencePort orderPort, DomainEventPublisher eventPublisher) {
return new CreateOrderService(orderPort, eventPublisher);
}
}
Mapping Strategy
Three models at each boundary — explicit mappers always:
REST DTO <-> Application Command/Response <-> Domain <-> Persistence Entity
Use MapStruct (@Mapper(componentModel = "spring")) or manual mappers. Never cross boundaries with one model.
Testing by Layer
| Layer | Test Type | What to Mock |
|---|
| Domain | Unit tests | Nothing — pure logic |
| Application | Unit tests | Output ports (mocked) |
| Infrastructure | Integration | Nothing (real DB/Kafka via Testcontainers) |
ArchUnit Enforcement
@ArchTest: domain must not depend on Spring/JPA, application must not depend on infrastructure, ports must be interfaces.
When to Use / Skip
Use: Complex logic, multiple input channels, long-lived projects, teams >3.
Skip: Simple CRUD, prototypes, <3 entities.
References
- references/hexagonal-patterns.md — Full examples: typed IDs, value objects, aggregate root, domain events, application service, adapters (Kafka, HTTP, DB), MapStruct persistence mapper, testing by layer, ArchUnit
- references/cqrs-patterns.md — CQRS: command/query separation, read model design, query adapter, anti-patterns
- references/solution-design.md — Solution Design template + Service Design template (Mermaid diagrams, C4, NFRs, deployment)
- references/event-sourcing.md — Event store design, aggregate root, snapshots, projections, decision matrix
Related Skills
- coding-standards — Package naming, method/class size limits
- messaging-patterns — Event-driven inter-context communication
- database-patterns — Repository adapters for persistence layer
- api-design — REST interface design for infrastructure adapters