clean-architecture

Provides implementation patterns for Clean Architecture, Hexagonal Architecture (Ports & Adapters), and Domain-Driven Design in Java 21+ Spring Boot 3.5+ applications. Use when structuring layered architectures, separating domain logic from frameworks, implementing ports and adapters, creating entities/value objects/aggregates, or refactoring monolithic codebases for testability and maintainability.

giuseppe-trisciuoglio/developer-kit2.6k installsMITSynced Aug 27

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: clean-architecture
description: Provides implementation patterns for Clean Architecture, Hexagonal Architecture (Ports & Adapters), and Domain-Driven Design in Java 21+ Spring Boot 3.5+ applications. Use when structuring layered architectures, separating domain logic from frameworks, implementing ports and adapters, creating entities/value objects/aggregates, or refactoring monolithic codebases for testability and maintainability.
license: MIT
---

# Clean Architecture, Hexagonal Architecture & DDD for Spring Boot

## Overview

This skill provides comprehensive guidance for implementing Clean Architecture, Hexagonal Architecture (Ports & Adapters), and Domain-Driven Design tactical patterns in Java 21+ Spring Boot 3.5+ applications. It ensures clear separation of concerns, framework-independent domain logic, and highly testable codebases through proper layering and dependency management.

## When to Use

- Architecting new Spring Boot applications with clear separation of concerns
- Refactoring tightly coupled code into testable, layered architectures
- Implementing domain logic independent of frameworks and infrastructure
- Designing ports and adapters for swappable implementations
- Applying Domain-Driven Design tactical patterns (entities, value objects, aggregates)
- Creating testable business logic without Spring context dependencies

## Instructions

### 1. Understand the Core Concepts

#### Clean Architecture Layers (Dependency Rule)

Dependencies flow inward. Inner layers know nothing about outer layers.

| Layer | Responsibility | Spring Boot Equivalent |
|-------|---------------|----------------------|
| **Domain** | Entities, value objects, domain events, repository interfaces | `domain/` - no Spring annotations |
| **Application** | Use cases, application services, DTOs, ports | `application/` - `@`Service, `@`Transactional |
| **Infrastructure** | Frameworks, database, external APIs | `infrastructure/` - `@`Repository, `@`Entity |
| **Adapter** | Controllers, presenters, external gateways | `adapter/` - `@`RestController |

#### Hexagonal Architecture (Ports & Adapters)

- **Domain Core**: Pure Java business logic, no framework dependencies
- **Ports**: Interfaces defining contracts (driven and driving)
- **Adapters**: Concrete implementations (JPA, REST, messaging)

#### Domain-Driven Design Tactical Patterns

- **Entities**: Objects with identity and lifecycle (e.g., `Order`, `Customer`)
- **Value Objects**: Immutable, defined by attributes (e.g., `Money`, `Email`)
- **Aggregates**: Consistency boundary with root entity
- **Domain Events**: Capture significant business occurrences
- **Repositories**: Persistence abstraction, implemented in infrastructure

### 2. Organize Package Structure

Follow this feature-based package organization:

```
com.example.order/
├── domain/
│   ├── model/              # Entities, value objects
│   ├── event/              # Domain events
│   ├── repository/         # Repository interfaces (ports)
│   └── exception/          # Domain exceptions
├── application/
│   ├── port/in/            # Driving ports (use case interfaces)
│   ├── port/out/           # Driven ports (external service interfaces)
│   ├── service/            # Application services
│   └── dto/                # Request/response DTOs
├── infrastructure/
│   ├── persistence/        # JPA entities, repository adapters
│   └── external/           # External service adapters
└── adapter/
    └── rest/               # REST controllers
```

### 3. Implement the Domain Layer (Framework-Free)

The domain layer must have zero dependencies on Spring or any framework.

- Use Java records for immutable value objects with built-in validation
- Place business logic in entities, not services (Rich Domain Model)
- Define repository interfaces (ports) in the domain layer
- Use strongly-typed IDs to prevent ID confusion
- Implement domain events for decoupling side effects
- Use factory methods for entity creation to enforce invariants

### 4. Implement the Application Layer

- Create use case interfaces (driving ports) in `application/port/in/`
- Create external service interfaces (driven ports) in `application/port/out/`
- Implement application services with `@Service` and `@Transactional`
- Use DTOs for request/response, separate from domain models
- Publish domain events after successful operations

### 5. Implement the Infrastructure Layer (Adapters)

- Create JPA entities in `infrastructure/persistence/`
- Implement repository adapters that map between domain and JPA entities
- Use MapStruct or manual mappers for domain-JPA conversion
- Configure conditional beans for swappable implementations
- Keep infrastructure concerns isolated from domain logic

### 6. Implement the Adapter Layer (REST)

- Create REST controllers in `adapter/rest/`
- Inject use case interfaces, not implementations
- Use Bean Validation on DTOs
- Return proper HTTP status codes and responses
- Handle exceptions with global exception handlers

### 7. Apply Best Practices

1. **Dependency Rule**: Domain has zero dependencies on Spring or other frameworks
2. **Immutable Value Objects**: Use Java records for value objects with built-in validation
3. **Rich Domain Models**: Place business logic in entities, not services
4. **Repository Pattern**: Domain defines interface, infrastructure implements
5. **Domain Events**: Decouple side effects from primary operations
6. **Constructor Injection**: Mandatory dependencies via final fields
7. **DTO Mapping**: Separate domain models from API contracts
8. **Transaction Boundaries**: Place `@`Transactional in application services
9. **Factory Methods**: Use `Entity.create()` for invariant enforcement during construction
10. **Separate JPA Entities**: Keep domain entities separate from JPA entities with mappers

### 8. Validate Architecture Compliance

After implementing each layer, verify the dependency rules are respected:

- **Domain Layer Check**: Run `grep -r "@Service\|@Component\|@Autowired" domain/` to ensure zero Spring imports
- **ArchUnit Test**: Add dependency tests to verify no infrastructure imports in domain layer:
  ```java
  noClasses().that().resideInPackage("..domain..")
      .should().accessClassesThat().resideInAnyPackage("..spring..", "..infrastructure..");
  ```
- **Entity Exposure Check**: Verify JPA entities are never returned from domain services
- **Transaction Check**: Confirm `@`Transactional only on application layer services, never on domain

### 9. Write Tests

- **Domain Tests**: Pure unit tests without Spring context, fast execution
- **Application Tests**: Unit tests with mocked ports using Mockito
- **Infrastructure Tests**: Integration tests with `@`DataJpaTest and Testcontainers
- **Adapter Tests**: Controller tests with `@`WebMvcTest

## Examples

### Example 1: Domain Layer - Entity with Domain Events

```java
// domain/model/Order.java
public class Order {
    private final OrderId id;
    private final List<OrderItem> items;
    private Money total;
    private OrderStatus status;
    private final List<DomainEvent> domainEvents = new ArrayList<>();

    private Order(OrderId id, List<OrderItem> items) {
        this.id = id;
        this.items = new ArrayList<>(items);
        this.status = OrderStatus.PENDING;
        calculateTotal();
    }

    public static Order create(List<OrderItem> items) {
        validateItems(items);
        Order order = new Order(OrderId.generate(), items);
        order.domainEvents.add(new OrderCreatedEvent(order.id, order.total));
        return order;
    }

    public void confirm() {
        if (status != OrderStatus.PENDING) {
            throw new DomainException("Only pending orders can be confirmed");
        }
        this.status = OrderStatus.CONFIRMED;
    }

    public List<DomainEvent> getDomainEvents() {
        return List.copyOf(domainEvents);
    }

    public void clearDomainEvents() {
        domainEvents.clear();
    }
}
```

### Example 2: Domain Layer - Value Object with Validation

```java
// domain/model/Money.java (Value Object)
public record Money(BigDecimal amount, Currency currency) {
    public Money {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new DomainException("Amount cannot be negative");
        }
    }

    public static Money zero() {
        return new Money(BigDecimal.ZERO, Currency.getInstance("EUR"));
    }

    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new DomainException("Currency mismatch");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
}
```

### Example 3: Domain Layer - Repository Port

```java
// domain/repository/OrderRepository.java (Port)
public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(OrderId id);
}
```

### Example 4: Application Layer - Use Case and Service

```java
// application/port/in/CreateOrderUseCase.java
public interface CreateOrderUseCase {
    OrderResponse createOrder(CreateOrderRequest request);
}

// application/dto/CreateOrderRequest.java
public record CreateOrderRequest(
    @NotNull UUID customerId,
    @NotEmpty List<OrderItemRequest> items
) {}

// application/service/OrderService.java
@Service
@RequiredArgsConstructor
@Transactional
public class OrderService implements CreateOrderUseCase {
    private final OrderRepository orderRepository;
    private final PaymentGateway paymentGateway;
    private final DomainEventPublisher eventPublisher;

    @Override
    public OrderResponse createOrder(CreateOrderRequest request) {
        List<OrderItem> items = mapItems(request.items());
        Order order = Order.create(items);

        PaymentResult payment = paymentGateway.charge(order.getTotal());
        if (!payment.successful()) {
            throw new PaymentFailedException("Payment failed");
        }

        order.confirm();
        Order saved = orderRepository.save(order);
        publishEvents(order);

        return OrderMapper.toResponse(saved);
    }

    private void publishEvents(Order order) {
        order.getDomainEvents().forEach(eventPublisher::publish);
        order.clearDomainEvents();
    }
}
```

### Example 5: Infrastructure Layer - JPA Entity and Adapter

```java
// infrastructure/persistence/OrderJpaEntity.java
@Entity
@Table(name = "orders")
public class OrderJpaEntity {
    @Id
    private UUID id;
    @Enumerated(EnumType.STRING)
    private OrderStatus status;
    private BigDecimal totalAmount;
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItemJpaEntity> items;
}

// infrastructure/persistence/OrderRepositoryAdapter.java
@Component
@RequiredArgsConstructor
public class OrderRepositoryAdapter implements OrderRepository {
    private final OrderJpaRepository jpaRepository;
    private final OrderJpaMapper mapper;

    @Override
    public Order save(Order order) {
        OrderJpaEntity entity = mapper.toEntity(order);
        return mapper.toDomain(jpaRepository.save(entity));
    }

    @Override
    public Optional<Order> findById(OrderId id) {
        return jpaRepository.findById(id.value()).map(mapper::toDomain);
    }
}
```

### Example 6: Adapter Layer - REST Controller

```java
// adapter/rest/OrderController.java
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
    private final CreateOrderUseCase createOrderUseCase;

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(
            @Valid @RequestBody CreateOrderRequest request) {
        OrderResponse response = createOrderUseCase.createOrder(request);
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(response.id())
            .toUri();
        return ResponseEntity.created(location).body(response);
    }
}
```

### Example 7: Domain Tests (No Spring Context)

```java
class OrderTest {
    @Test
    void shouldCreateOrderWithValidItems() {
        List<OrderItem> items = List.of(
            new OrderItem(new ProductId(UUID.randomUUID()), 2, new Money("10.00", EUR))
        );

        Order order = Order.create(items);

        assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING);
        assertThat(order.getDomainEvents()).hasSize(1);
    }
}
```

### Example 8: Application Tests (Unit with Mocks)

```java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock OrderRepository orderRepository;
    @Mock PaymentGateway paymentGateway;
    @Mock DomainEventPublisher eventPublisher;

    @InjectMocks OrderService orderService;

    @Test
    void shouldCreateAndConfirmOrder() {
        when(paymentGateway.charge(any())).thenReturn(new PaymentResult(true, "tx-123"));
        when(orderRepository.save(any())).thenAnswer(i -> i.getArgument(0));

        OrderResponse response = orderService.createOrder(createRequest());

        assertThat(response.status()).isEqualTo(OrderStatus.CONFIRMED);
        verify(eventPublisher).publish(any(OrderCreatedEvent.class));
    }
}
```

## Best Practices

- **Domain purity**: Keep the domain layer free of Spring annotations and framework imports — zero dependencies on outer layers
- **Feature-based packages**: Organize by business capability (`order/`, `customer/`) rather than technical role, with each feature containing all four layers
- **Immutable value objects**: Use Java records for value objects with built-in validation in compactors — immutable by design
- **Rich domain models**: Place business logic in entities and aggregates, not in application services — services orchestrate, entities encapsulate
- **Always map**: Separate JPA entities from domain models using MapStruct or manual mappers; never expose JPA entities outside infrastructure
- **Domain events for decoupling**: Use `DomainEventPublisher` to decouple cross-aggregate side effects instead of direct service calls
- **Transaction boundaries in application layer**: Place `@Transactional` only on application services, never on domain classes
- **Factory methods for invariants**: Use `Entity.create(...)` static methods to enforce invariants at construction time
- **Enforce with ArchUnit**: Add ArchUnit tests in the test suite to verify no Spring or infrastructure imports reach the domain layer
- **Strongly-typed IDs**: Use `record OrderId(UUID value)` instead of raw `UUID` to prevent ID confusion across aggregates

## Constraints and Warnings

### Critical Constraints

- **Domain Layer Purity**: Never add Spring annotations (`@Entity`, `@Autowired`, `@Component`) to domain classes
- **Dependency Direction**: Dependencies must only point inward (domain <- application <- infrastructure/adapter)
- **Framework Isolation**: All framework-specific code must stay in infrastructure and adapter layers

### Common Pitfalls to Avoid

- **Anemic Domain Model**: Entities with only getters/setters, logic in services - place business logic in entities
- **Framework Leakage**: `@Entity`, `@Autowired` in domain layer - keep domain framework-free
- **Lazy Loading Issues**: Exposing JPA entities through domain model - use mappers to convert
- **Circular Dependencies**: Between domain aggregates - use IDs instead of direct references
- **Missing Domain Events**: Direct service calls instead of events for cross-aggregate communication
- **Repository Misplacement**: Defining repository interfaces in infrastructure - they belong in domain
- **DTO Bypass**: Exposing domain entities directly in API - always use DTOs for external contracts

### Performance Considerations

- Separate JPA entities from domain models to avoid lazy loading issues
- Use read-only transactions for query operations
- Consider CQRS for complex read/write scenarios

## References

- `references/java-clean-architecture.md` - Java-specific patterns (records, sealed classes, strongly-typed IDs)
- `references/spring-boot-implementation.md` - Spring Boot integration (DI patterns, JPA mapping, transaction management)

More Refactoring skills

← All Refactoring skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY