clean-architecture

Provides implementation patterns for Clean Architecture, Hexagonal Architecture (Ports & Adapters), and Domain-Driven Design in Java 21+ Spring Boot 3.5+…

INSTALLATION
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill clean-architecture
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

#### 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

  • 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 Entity.create() for invariant enforcement during construction
  • 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:
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/, 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)
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card