Spring Boot 4 data layer implementation for Domain-Driven Design. Use when implementing JPA or JDBC aggregates, Spring Data repositories, transactional services, projections, or entity auditing. Covers aggregate roots with AbstractAggregateRoot, value object mapping, EntityGraph for N+1 prevention, and Spring Boot 4 specifics (JSpecify null-safety, AOT repositories). For DDD concepts and design decisions, see the domain-driven-design skill.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
references/aggregates.mdreferences/repositories.mdreferences/transactions.mdImplements DDD tactical patterns with Spring Data JPA and Spring Data JDBC in Spring Boot 4.
| Choose | When |
|---|---|
| Spring Data JPA | Complex queries, existing Hibernate expertise, need lazy loading |
| Spring Data JDBC | DDD-first design, simpler mapping, aggregate-per-table, no lazy loading |
Spring Data JDBC enforces aggregate boundaries naturally—recommended for new DDD projects.
AbstractAggregateRoot<T> for domain events@Embedded or @Converter for immutability@Transactional on public methods, one aggregate per transaction@Entity
public class Order extends AbstractAggregateRoot<Order> {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
private CustomerId customerId; // Value object
@Enumerated(EnumType.STRING)
private OrderStatus status = OrderStatus.DRAFT;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "order_id")
private Set<OrderLine> lines = new HashSet<>();
public void submit() {
if (lines.isEmpty()) throw new IllegalStateException("Empty order");
this.status = OrderStatus.SUBMITTED;
registerEvent(new OrderSubmitted(this.id));
}
}
@Entity
class Order : AbstractAggregateRoot<Order>() {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
private set
@Embedded
lateinit var customerId: CustomerId
private set
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
@JoinColumn(name = "order_id")
private val _lines: MutableSet<OrderLine> = mutableSetOf()
val lines: Set<OrderLine> get() = _lines.toSet()
fun submit(): Order {
check(_lines.isNotEmpty()) { "Empty order" }
status = OrderStatus.SUBMITTED
registerEvent(OrderSubmitted(id!!))
return this
}
}
public interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(attributePaths = {"lines", "lines.product"})
Optional<Order> findWithLinesById(Long id);
List<OrderSummary> findByStatus(OrderStatus status); // Projection
@Query("SELECT o FROM Order o WHERE o.customerId.value = :customerId")
List<Order> findByCustomerId(@Param("customerId") String customerId);
}
@Service
@Transactional
public class OrderService {
private final OrderRepository orders;
@Transactional(readOnly = true)
public OrderDto findById(Long id) {
return orders.findById(id)
.map(OrderDto::from)
.orElseThrow(() -> new OrderNotFoundException(id));
}
public OrderDto submit(Long orderId) {
Order order = orders.findWithLinesById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.submit();
return OrderDto.from(orders.save(order));
}
}
@NullMarked // All params/returns non-null by default
@Service
public class OrderService {
public @Nullable Order findByIdOrNull(Long id) {
return orders.findById(id).orElse(null);
}
}
Enabled by default—query methods compile to source code for faster startup. No configuration needed.
All imports use jakarta.*:
jakarta.persistence.* (JPA)jakarta.validation.* (Bean Validation)jakarta.transaction.Transactional (or Spring's)| Anti-Pattern | Fix |
|---|---|
FetchType.EAGER on associations | Use LAZY + @EntityGraph when needed |
| Returning entities from controllers | Convert to DTOs in service layer |
@Transactional on private methods | Use public methods (proxy limitation) |
Missing readOnly = true on queries | Add for read operations (performance) |
| Direct aggregate-to-aggregate references | Reference by ID only |
| Multiple aggregates in one transaction | Use domain events for eventual consistency |
repository.save() before events dispatch@DataJpaTest — Use TestEntityManager for setup