Spring Boot Skills
GitHub
Architecture

Hexagonal Architecture

Use when the project follows hexagonal (ports & adapters) architecture. Prevents domain code from depending on Spring or JPA. Use when you see packages like domain/, application/, infrastructure/, or adapters/ in the project structure.

View SKILL.md

Package Structure

src/main/java/com/example/
├── domain/                     ← Pure Java. Zero framework dependencies.
│   ├── model/                  ← Entities, value objects, aggregates
│   ├── port/
│   │   ├── in/                 ← Use case interfaces (driving ports)
│   │   └── out/                ← Repository/external interfaces (driven ports)
│   └── service/                ← Domain services (pure business logic)
├── application/                ← Orchestrates use cases. Spring allowed here.
│   └── usecase/                ← @Service implementations of domain ports
└── infrastructure/             ← All framework/DB/HTTP details
    ├── persistence/            ← JPA adapters implementing out ports
    ├── web/                    ← REST controllers (driving adapters)
    └── external/               ← HTTP clients, messaging adapters

Domain Layer — Zero Spring

// domain/model/Order.java — pure Java, no annotations
public class Order {
    private final OrderId id;
    private final CustomerId customerId;
    private OrderStatus status;
    private final List<OrderItem> items;

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

    public static Order create(CustomerId customerId) {
        return new Order(OrderId.generate(), customerId);
    }

    public void addItem(ProductId productId, int quantity, Money price) {
        if (status != OrderStatus.PENDING)
            throw new OrderNotModifiableException(id);
        items.add(new OrderItem(productId, quantity, price));
    }

    // Getters only — no setters
}

// domain/model/OrderId.java — value object
public record OrderId(UUID value) {
    public static OrderId generate() { return new OrderId(UUID.randomUUID()); }
    public static OrderId of(String value) { return new OrderId(UUID.fromString(value)); }
}

Ports — Interfaces Only

// domain/port/in/CreateOrderUseCase.java — driving port
public interface CreateOrderUseCase {
    Order createOrder(CreateOrderCommand command);
}

// domain/port/in/CreateOrderCommand.java
public record CreateOrderCommand(CustomerId customerId, List<OrderItemData> items) {}

// domain/port/out/OrderRepository.java — driven port
public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(OrderId id);
    void deleteById(OrderId id);
}

// domain/port/out/InventoryPort.java — driven port
public interface InventoryPort {
    void reserve(List<OrderItem> items);
    void release(List<OrderItem> items);
}

Application Layer — Use Case Implementation

// application/usecase/CreateOrderService.java
@Service  // Spring allowed here
@RequiredArgsConstructor
@Transactional
public class CreateOrderService implements CreateOrderUseCase {

    private final OrderRepository orderRepository;   // domain port (not JPA repo)
    private final InventoryPort inventoryPort;        // domain port

    @Override
    public Order createOrder(CreateOrderCommand command) {
        Order order = Order.create(command.customerId());
        command.items().forEach(item ->
            order.addItem(item.productId(), item.quantity(), item.price()));
        inventoryPort.reserve(order.getItems());
        return orderRepository.save(order);
    }
}

Infrastructure — Adapters

// infrastructure/persistence/JpaOrderRepository.java — implements domain port
@Repository
@RequiredArgsConstructor
public class JpaOrderRepository implements OrderRepository {

    private final SpringDataOrderRepository springDataRepo;
    private final OrderMapper mapper;

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

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

    @Override
    public void deleteById(OrderId id) {
        springDataRepo.deleteById(id.value());
    }
}

// Separate Spring Data interface — infrastructure detail
interface SpringDataOrderRepository extends JpaRepository<OrderJpaEntity, UUID> {}

// infrastructure/web/OrderController.java — driving adapter
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
public class OrderController {

    private final CreateOrderUseCase createOrderUseCase; // injects use case port

    @PostMapping
    public ResponseEntity<ApiResponse<OrderResponse>> create(@Valid @RequestBody CreateOrderRequest request) {
        Order order = createOrderUseCase.createOrder(request.toCommand());
        return ResponseEntity.status(201).body(ApiResponse.ok(OrderResponse.from(order)));
    }
}

Gotchas

  • Agent imports jakarta.persistence in domain classes — domain must be framework-free
  • Agent injects JpaRepository directly into use cases — use domain port interfaces
  • Agent puts @Transactional on domain services — belongs in application layer
  • Agent mixes driving and driven ports — port/in = what app offers, port/out = what app needs
  • Agent creates anemic domain with only getters/setters — behavior belongs on domain objects
  • Agent stubs driven ports with @MockBean in tests — removed in Boot 4; use @MockitoBean
  • Agent puts the adapter's spring-boot-starter-aop dependency for port proxies — renamed spring-boot-starter-aspectj in Boot 4
FROM THE REPOSITORY

Examples & templates

bad-use-case.javaexamples
Source on GitHub
// ❌ BAD — infrastructure leaks into domain, anemic model, no ports

@Service
@RequiredArgsConstructor
public class CreateOrderService {

    private final OrderJpaRepository orderRepository;     // JPA repo directly — no domain port
    private final RestTemplate restTemplate;               // infrastructure in application layer

    @Transactional
    public OrderEntity createOrder(CreateOrderRequest request) {
        // Business logic in service, not domain model (anemic)
        if (request.getItems().isEmpty()) {
            throw new RuntimeException("No items");       // generic exception, no domain meaning
        }

        OrderEntity order = new OrderEntity();            // JPA entity used as domain model
        order.setCustomerEmail(request.getEmail());       // setter-based, no invariant enforcement
        order.setStatus("PENDING");                       // string instead of enum/value object

        for (ItemRequest item : request.getItems()) {
            OrderItemEntity orderItem = new OrderItemEntity();  // direct child construction
            orderItem.setProductId(item.getProductId());
            orderItem.setQuantity(item.getQuantity());
            order.getItems().add(orderItem);              // bypasses aggregate root
        }

        // Infrastructure call mixed with business logic
        restTemplate.postForEntity(                       // HTTP call in service, not behind port
            "http://inventory-service/reserve",
            request.getItems(), Void.class);

        return orderRepository.save(order);               // returns JPA entity, not domain model
    }
}

// Domain model polluted with JPA
@Entity                                                   // JPA annotation in domain model
@Data                                                     // generates setters — no encapsulation
public class OrderEntity {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;                                      // Long ID, not typed value object
    private String customerEmail;
    private String status;                                // String instead of OrderStatus enum
    @OneToMany private List<OrderItemEntity> items;       // missing cascade, orphanRemoval
}
good-use-case.javaexamples
Source on GitHub
// ✅ GOOD — use case implements port, injects domain ports, no Spring annotations in domain

// --- Domain port (in domain layer, no Spring imports) ---
public interface CreateOrderUseCase {
    Order execute(CreateOrderCommand command);
}

public interface OrderRepository {  // domain port — not Spring's JpaRepository
    Order save(Order order);
    Optional<Order> findById(OrderId id);
}

// --- Application layer (implements use case, orchestrates domain) ---
@Service
@RequiredArgsConstructor
@Transactional
public class CreateOrderService implements CreateOrderUseCase {

    private final OrderRepository orderRepository;        // domain port, not JPA
    private final InventoryPort inventoryPort;             // domain port for external system

    @Override
    public Order execute(CreateOrderCommand command) {
        inventoryPort.reserve(command.items());
        Order order = Order.create(command.customerEmail(), command.items());
        return orderRepository.save(order);
    }
}

// --- Domain model (no JPA annotations, pure business logic) ---
public class Order {
    private final OrderId id;
    private final EmailAddress customerEmail;
    private OrderStatus status;
    private final List<OrderItem> items;

    public static Order create(EmailAddress email, List<OrderItemCommand> items) {
        Order order = new Order(OrderId.generate(), email, OrderStatus.PENDING, new ArrayList<>());
        items.forEach(i -> order.addItem(i.productId(), i.quantity()));
        return order;
    }

    public void addItem(ProductId productId, int quantity) {
        if (status != OrderStatus.PENDING)
            throw new OrderNotModifiableException(id);
        items.add(new OrderItem(productId, quantity));
    }
}
OrderJpaAdapter.javatemplates
Source on GitHub
package com.example.order.infrastructure.persistence;

import com.example.order.domain.model.Order;
import com.example.order.domain.model.OrderId;
import com.example.order.domain.port.out.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Optional;

/**
 * Persistence adapter — implements domain port using JPA.
 * Domain layer never sees JPA, only this adapter does.
 */
@Component
@RequiredArgsConstructor
public class OrderJpaAdapter implements OrderRepository {

    private final SpringDataOrderRepository jpaRepository;
    private final OrderPersistenceMapper mapper;

    @Override
    public Order save(Order order) {
        OrderJpaEntity entity = mapper.toJpaEntity(order);
        OrderJpaEntity saved = jpaRepository.save(entity);
        return mapper.toDomainModel(saved);
    }

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

    @Override
    public void deleteById(OrderId id) {
        jpaRepository.deleteById(id.value());
    }
}

/**
 * Spring Data JPA repository — internal to infrastructure layer.
 * Never exposed outside this package.
 */
interface SpringDataOrderRepository extends org.springframework.data.jpa.repository.JpaRepository<OrderJpaEntity, java.util.UUID> {
}
OrderJpaEntity.javatemplates
Source on GitHub
package com.example.order.infrastructure.persistence;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * JPA entity — lives in infrastructure layer, separate from domain model.
 * Maps between database schema and persistence. Never used in domain logic.
 */
@Entity
@Table(name = "orders")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderJpaEntity {

    @Id
    @Column(updatable = false, nullable = false)
    private UUID id;

    @Column(name = "customer_email", nullable = false)
    private String customerEmail;

    // Stored as the enum's name; the persistence mapper converts to/from OrderStatus.
    // (@Enumerated only applies to enum-typed fields — on a String Hibernate fails at startup.)
    @Column(nullable = false)
    private String status;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItemJpaEntity> items = new ArrayList<>();

    @CreationTimestamp
    @Column(updatable = false)
    private Instant createdAt;

    @UpdateTimestamp
    private Instant updatedAt;

    @Version
    private Long version;

    // Used by mapper only — no public constructor
    OrderJpaEntity(UUID id, String customerEmail, String status) {
        this.id = id;
        this.customerEmail = customerEmail;
        this.status = status;
    }

    void addItem(OrderItemJpaEntity item) {
        items.add(item);
        item.setOrder(this);
    }
}

Verification scope

Compiled sources
No
Runtime checks
No

Coverage describes checked-in fixtures, not a guarantee for every snippet or project.

Coverage matrix & agent evidence
ADD TO YOUR AGENT

Install Boot 4 skills

claude plugin marketplace add rrezartprebreza/spring-boot-skills
claude plugin install spring-boot-4-skills@spring-boot-skills