Skip to content
Back to skills

Architecture

ASecurity

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.

  • 4 stars
  • 0 votes
  • 0 copies
  • 5 views
  • Added May 26, 2026
developmentrustgojavaspringtestingapidatabase

Works with

  • api

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned May 27, 2026

npx -y skills add taipt1504/agent-skills --skill architecture --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Architecture?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Architecture
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/taipt1504-architecture/badge)](https://www.skillsdirectory.com/skills/taipt1504-architecture)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.

```java
public class Order {
    private final OrderId id;
    private OrderStatus status;
    private final List<DomainEvent> domainEvents = new ArrayList<>();

    // Factory: new instance (with validation)
    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;
    }

    // Factory: reconstitute from DB (bypasses validation)
    public static Order reconstitute(OrderId id, CustomerId customerId,
                                      List<OrderLine> lines, OrderStatus status) {
        return new Order(id, customerId, lines, status);
    }

    // Business method: state transition + event
    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:

```java
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

```java
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
```

```java
// Command (immutable record)
public record CreateOrderCommand(String customerId, List<OrderLineDto> lines) {}

// Use case port (application layer interface)
public interface CreateOrderUseCase {
    Mono<OrderId> execute(CreateOrderCommand command);
}

// Handler (application layer implementation)
@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
```

```java
// Query use case (separate from command)
public interface GetOrderQuery {
    Mono<OrderDetailResponse> execute(String orderId);
}

// Read model — flat, denormalized, no domain logic
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

```java
// Port (application layer)
public interface DomainEventPublisher {
    void publishAll(List<DomainEvent> events);
}

// Adapter (infrastructure layer — Kafka implementation)
@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)

```java
// Input port — what the outside world can ask
public interface CreateOrderUseCase {
    Mono<OrderId> execute(CreateOrderCommand command);
}

// Output port — what the application needs from infrastructure
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)

```java
// Inbound adapter (REST controller → uses input port)
@RestController
@RequiredArgsConstructor
public class OrderController {
    private final CreateOrderUseCase createOrder;  // depends on PORT, not service

    @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));
    }
}

// Outbound adapter (implements output port → talks to DB)
@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)

```java
@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](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](references/cqrs-patterns.md)** — CQRS: command/query separation, read model design, query adapter, anti-patterns
- **[references/solution-design.md](references/solution-design.md)** — Solution Design template + Service Design template (Mermaid diagrams, C4, NFRs, deployment)
- **[references/event-sourcing.md](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

Files in this skill

  • SKILL.md10.9 KB
  • references/cqrs-patterns.md6.4 KB
  • references/event-sourcing.md7.7 KB
  • references/hexagonal-patterns.md11.3 KB
  • references/solution-design.md4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.