Developer-kit 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.
git clone https://github.com/giuseppe-trisciuoglio/developer-kit
T=$(mktemp -d) && git clone --depth=1 https://github.com/giuseppe-trisciuoglio/developer-kit "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/developer-kit-java/skills/clean-architecture" ~/.claude/skills/giuseppe-trisciuoglio-developer-kit-clean-architecture && rm -rf "$T"
plugins/developer-kit-java/skills/clean-architecture/SKILL.mdClean 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 | - no Spring annotations |
| Application | Use cases, application services, DTOs, ports | - Service, Transactional |
| Infrastructure | Frameworks, database, external APIs | - Repository, Entity |
| Adapter | Controllers, presenters, external gateways | - 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
and@Service@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
- Dependency Rule: Domain has zero dependencies on Spring or other frameworks
- Immutable Value Objects: Use Java records for value objects with built-in validation
- Rich Domain Models: Place business logic in entities, not services
- Repository Pattern: Domain defines interface, infrastructure implements
- Domain Events: Decouple side effects from primary operations
- Constructor Injection: Mandatory dependencies via final fields
- DTO Mapping: Separate domain models from API contracts
- Transaction Boundaries: Place
Transactional in application services@ - Factory Methods: Use
for invariant enforcement during constructionEntity.create() - 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
to ensure zero Spring importsgrep -r "@Service\|@Component\|@Autowired" domain/ - ArchUnit Test: Add dependency tests to verify no infrastructure imports in domain layer:
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
// 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
// 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
// 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
// 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
// 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
// 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)
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)
@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/
) rather than technical role, with each feature containing all four layerscustomer/ - 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
to decouple cross-aggregate side effects instead of direct service callsDomainEventPublisher - Transaction boundaries in application layer: Place
only on application services, never on domain classes@Transactional - Factory methods for invariants: Use
static methods to enforce invariants at construction timeEntity.create(...) - 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
instead of rawrecord OrderId(UUID value)
to prevent ID confusion across aggregatesUUID
Constraints and Warnings
Critical Constraints
- Domain Layer Purity: Never add Spring annotations (
,@Entity
,@Autowired
) to domain classes@Component - 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
in domain layer - keep domain framework-free@Autowired - 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
- Java-specific patterns (records, sealed classes, strongly-typed IDs)references/java-clean-architecture.md
- Spring Boot integration (DI patterns, JPA mapping, transaction management)references/spring-boot-implementation.md