ddd-building-blocks: DDD-абстракции для Java
ddd-building-blocks: библиотека DDD-абстракций для Java.
ddd-building-blocks Java — когда начинаешь применять DDD в Java-проекте, первый вопрос — где взять базовые абстракции? Писать свои Entity, AggregateRoot, DomainEvent в каждом проекте заново? Тащить Spring Data или Axon ради пары интерфейсов?
Я написал библиотеку ddd-building-blocks — минимальный набор абстракций для DDD. Zero dependencies, чистая Java. Подключаешь одну зависимость — и получаешь готовый каркас для доменной модели.
Если вы ещё не знакомы с паттернами DDD, рекомендую начать с вводной статьи. Каждая абстракция библиотеки реализует конкретный тактический паттерн, подробно разобранный в статье о тактических паттернах.
ddd-building-blocks Java: подключение
dependencies {
implementation("ru.badgermock:ddd-building-blocks:1.0.0")
}
Что внутри
Библиотека содержит 8 абстракций — ровно столько, сколько нужно для полноценной доменной модели, и ни одной лишней.
Entity — сущность с идентичностью
Entity идентифицируется по ID, а не по атрибутам. В библиотеке equals/hashCode реализованы в базовом классе и финализированы — переопределить нельзя. Это исключает частую ошибку, когда разработчик сравнивает сущности по полям вместо ID.
public class Product extends Entity {
private final ProductId id;
private String name;
private Money price;
public Product(ProductId id, String name, Money price) {
this.id = id;
this.name = name;
this.price = price;
}
@Override
public ProductId getId() {
return id;
}
public void changePrice(Money newPrice) {
this.price = newPrice;
}
}
// Два Product с одинаковым ProductId — одна и та же сущность
Product a = new Product(productId, "Молоко", money(99));
Product b = new Product(productId, "Молоко", money(149));
a.equals(b); // true — тот же ID
ValueObject — объект-значение
Value Object определяется атрибутами, иммутабелен. Реализуется через Java record — equals, hashCode и toString генерируются автоматически.
public record Money(BigDecimal amount, String currency) implements ValueObject {
public Money {
Objects.requireNonNull(amount, "amount must not be null");
Objects.requireNonNull(currency, "currency must not be null");
}
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money(amount.add(other.amount), currency);
}
}
AggregateRoot — корень агрегата
Aggregate Root — граница консистентности. Все изменения вложенных Entity и ValueObject проходят через корень. В библиотеке AggregateRoot поддерживает доменные события (registerEvent) и версионирование для оптимистичной блокировки.
public class Order extends AggregateRoot {
private final OrderId id;
private final List lines = new ArrayList<>();
private OrderStatus status;
public Order(OrderId id) {
this.id = id;
this.status = OrderStatus.DRAFT;
}
@Override
public OrderId getId() {
return id;
}
public void place() {
if (lines.isEmpty()) {
throw new IllegalStateException("Cannot place an empty order");
}
this.status = OrderStatus.PLACED;
registerEvent(new OrderPlacedEvent(id, calculateTotal()));
}
public void addLine(ProductId productId, int quantity, Money price) {
lines.add(new OrderLine(productId, quantity, price));
}
}
DomainEvent — доменное событие
Domain Event фиксирует факт, который произошёл в домене. Несёт информацию об агрегате-источнике. Два конструктора: для создания новых событий (UUID и timestamp генерируются автоматически) и для восстановления из хранилища.
public class OrderPlacedEvent extends DomainEvent {
private final Money totalAmount;
// Новое событие
public OrderPlacedEvent(OrderId orderId, Money totalAmount) {
super("Order", orderId.toString());
this.totalAmount = totalAmount;
}
// Восстановление из хранилища / message bus
public OrderPlacedEvent(UUID eventId, Instant occurredAt,
String aggregateId, Money totalAmount) {
super(eventId, occurredAt, "Order", aggregateId);
this.totalAmount = totalAmount;
}
public Money getTotalAmount() {
return totalAmount;
}
}
DomainEventPublisher и DomainEventHandler
Домен определяет контракт публикации, инфраструктура реализует. DomainEventHandler — функциональный интерфейс, можно использовать как лямбду. Подробнее о подписчиках и @TransactionalEventListener — в статье об интеграционных паттернах.
// Реализация через Spring Events
public class SpringEventPublisher implements DomainEventPublisher {
private final ApplicationEventPublisher applicationEventPublisher;
public SpringEventPublisher(ApplicationEventPublisher publisher) {
this.applicationEventPublisher = publisher;
}
@Override
public void publish(DomainEvent event) {
applicationEventPublisher.publishEvent(event);
}
}
// Типизированный обработчик
public class SendConfirmationOnOrderPlaced
implements DomainEventHandler {
private final NotificationService notifications;
@Override
public void handle(OrderPlacedEvent event) {
notifications.sendOrderConfirmation(
event.getAggregateId(),
event.getTotalAmount()
);
}
}
AggregateRepository — репозиторий агрегатов
Интерфейс персистенции для write-side. Работает только с AggregateRoot. Блокировки — ответственность реализации. Для read-side (запросы, DTO, проекции) общая абстракция не предусмотрена — каждый сервис определяет свои query-интерфейсы напрямую через jOOQ/SQL. Это стандартный подход CQRS: write-side формализован, read-side свободен.
// Доменный интерфейс
public interface OrderRepository extends AggregateRepository {
List findByStatus(OrderStatus status);
// Намерение, не механизм
Optional findByIdForModification(OrderId id);
}
// Реализация на jOOQ с публикацией событий
public class JooqOrderRepository implements OrderRepository {
private final DSLContext dsl;
private final DomainEventPublisher eventPublisher;
@Override
public Order save(Order aggregate) {
dsl.update(ORDERS)
.set(ORDERS.STATUS, aggregate.getStatus().name())
.set(ORDERS.TOTAL_AMOUNT, aggregate.getTotalAmount())
.set(ORDERS.UPDATED_AT, LocalDateTime.now())
.where(ORDERS.ID.eq(aggregate.getId().getValue()))
.execute();
eventPublisher.publishAll(aggregate.getEvents());
aggregate.clearDomainEvents();
return aggregate;
}
@Override
public Optional findByIdForModification(OrderId id) {
return dsl.selectFrom(ORDERS)
.where(ORDERS.ID.eq(id.getValue()))
.forUpdate()
.fetchOptional(this::toOrder);
}
}
Specification — спецификация бизнес-правил
Specification инкапсулирует бизнес-правила. Поддерживает композицию через and, or, not.
public class OrderIsOverdue extends Specification {
@Override
public boolean isSatisfiedBy(Order order) {
return order.getDueDate().isBefore(Instant.now());
}
}
public class OrderIsUnpaid extends Specification {
@Override
public boolean isSatisfiedBy(Order order) {
return !order.isPaid();
}
}
// Композиция
Specification needsAttention = new OrderIsOverdue()
.and(new OrderIsUnpaid());
List urgent = orders.stream()
.filter(needsAttention::isSatisfiedBy)
.toList();
Почему не Spring Data / Axon / jMolecules?
Существующие решения либо слишком тяжёлые, либо привязывают к конкретному фреймворку:
- Spring Data —
AbstractAggregateRootпривязан к Spring. Нельзя использовать в модуле без Spring-контекста. - Axon Framework — полноценный CQRS/ES-фреймворк. Избыточен, если нужны только базовые абстракции.
- jMolecules — аннотации для документирования, но не несут поведения (
equals/hashCodeдля Entity,registerEventдля Aggregate).
ddd-building-blocks — это золотая середина: реальное поведение (не просто маркерные аннотации), zero dependencies (работает с любым фреймворком) и минимальный API (8 абстракций, которые помещаются в голове).
Связь с серией статей
Каждая абстракция библиотеки реализует конкретный паттерн DDD. Вот карта соответствий:
Entity→ Entity (тактические паттерны)ValueObject→ Value Object (тактические паттерны)AggregateRoot→ Aggregate (тактические паттерны)DomainEvent+DomainEventPublisher+DomainEventHandler→ Domain Event (тактические паттерны) и интеграционные паттерныAggregateRepository→ Repository (тактические паттерны)Specification→ Specification (тактические паттерны)
Принципы проектирования API библиотеки (Intention-Revealing Interfaces, Closure of Operations) описаны в статье Принципы проектирования в DDD.
Репозиторий: github.com/remodov/ddd-building-blocks
Пакет: ru.badgermock:ddd-building-blocks:1.0.0