Опирается на правила:
R-MOD-1…R-MOD-2из DDD Tactical Style Guide → раздел 9. Module (структура пакетов).
Важно знать
- Верхний уровень группировки в
core/— Bounded Context, не тип объекта.core/order/...,core/customer/..., неentity/, неservice/, неrepository/.- Внутри BC —
domain/(aggregate, entity, valueobject, event, repository-интерфейс, service, specification) иusecase/(command, query).- Адаптеры — отдельная иерархия:
adapter/in/rest/...,adapter/out/postgres/...,adapter/out/kafka/.... В multi-module Gradle это отдельные модули.- Группировка по типу — антипаттерн. Папки
entity/,repository/,service/на верхнем уровне — флаг плохого дизайна.core/не зависит от Spring/JPA/jOOQ/Hibernate. В импортах domain-классов нетorg.springframework.*,org.jooq.*,jakarta.persistence.*.- Запрет на
@Entity,@Table,@Component,@Autowired,@RestControllerвнутриdomain/. Толькоddd-building-blocks-импорты и стандартная Java.- В нашем варианте
@Componentдопускается вcore/на Уровне 2 (см. Hexagonal: когда переходить) — на Уровне 3 уезжает в bootstrap.
Структура пакетов выглядит мелочью, но именно по ней через год определяется, был ли в проекте DDD или ярлык. Группировка по типу (entity/, service/, repository/) выглядит «опрятно» в момент создания, но через 50 классов разработчик ищет «всё про Order» в шести разных папках. Группировка по домену даёт обратное — открыл core/order/, видишь всё, что есть про заказ. Раскрытие раздела 9 гайда.
Канонический layout
R-MOD-1: верхний уровень в core/ — Bounded Context. Внутри — domain/ и usecase/.
core/
order/ # Bounded Context: Order
domain/
aggregate/
Order.java # AggregateRoot
entity/
OrderItem.java # внутренняя Entity агрегата
valueobject/
Money.java
OrderId.java
OrderStatus.java # enum, тоже сюда (фактически VO)
event/
OrderCreated.java
OrderConfirmed.java
OrderCancelled.java
repository/
OrderRepository.java # interface, наследует AggregateRepository
service/ # опционально
PricingService.java
specification/ # опционально
EligibleForRefundSpec.java
factory/ # опционально
OrderFactory.java
usecase/
command/
CreateOrder.java
CreateOrderHandler.java
ConfirmOrder.java
ConfirmOrderHandler.java
CancelOrder.java
CancelOrderHandler.java
query/
GetOrder.java
GetOrderHandler.java
FindActiveOrders.java
FindActiveOrdersHandler.java
customer/ # Bounded Context: Customer
domain/...
usecase/...
product/ # Bounded Context: Product
domain/...
usecase/...
adapter/
in/
rest/
OrderController.java
OrderMapper.java # REST DTO ↔ Command/Query
out/
postgres/
JooqOrderRepository.java
OrderDomainRecordMapper.java
kafka/
KafkaDomainEventPublisher.java
Что даёт такая структура:
- Перенос BC в отдельный сервис — пакет ⇒ модуль. Когда
Customerрастёт и хочется выделить, копируетсяcore/customer/...целиком в новый сервис. Внутренние ссылки на другие BC уже идут через ID (R-AGG-5), импорты не размазаны. - Чтение «всё про Order» — одна папка. Открыл
core/order/, видишьOrder, события, репозиторий, use-case-ы. Не надо искатьOrderServiceвservices/,OrderRepositoryвrepositories/,OrderEntityвentities/. - ArchUnit-тест. Можно написать правило «классы из
core/order/...не зависят отcore/customer/...», и оно отражает реальную границу BC.
Группировка по типу — антипаттерн
R-MOD-1 явно запрещает:
// ПЛОХО — package-by-layer
src/main/java/com/example/
entity/
Order.java
Customer.java
Product.java
repository/
OrderRepository.java
CustomerRepository.java
ProductRepository.java
service/
OrderService.java
CustomerService.java
PricingService.java
controller/
OrderController.java
CustomerController.java
Что не так:
- Зависимости размазаны. Чтобы понять «как работает Order», нужно открыть 4 разные папки.
- Cross-BC связь не видна. Импорт
entity.CustomerвOrderService— формально ок, но это нарушениеR-AGG-5(ссылка между агрегатами объектом). Структура по типу не помогает заметить. - Refactor BC = пересборка всего проекта. Перенос
Customerв отдельный сервис — нужно вытаскивать класс изentity/,repository/,service/,controller/. Не пакетная операция.
Группировка по типу — наследие старого Java enterprise-стиля (com.example.dao, com.example.bo, com.example.dto). В DDD от неё отказываются по принципу «high cohesion, loose coupling»: всё, что связано одним доменом, должно быть рядом.
core/ без Spring/JPA/jOOQ
R-MOD-2: пакеты core/<bc>/domain/... и core/<bc>/usecase/... не импортируют persistence-стек и фреймворк.
Что разрешено в импортах:
java.*— стандартная библиотека.org.remodov.dddbuildingblocks.*— наша библиотека DDD-абстракций.lombok.*—@Getter,@RequiredArgsConstructor,@Value(см. Java Style Guide).- Другие domain-классы того же или соседнего BC (по ID).
Что не допускается:
org.springframework.*— кроме@Componentна Уровне 2 (см. ниже).jakarta.persistence.*(@Entity,@Table,@Column) — JPA-аннотации не вdomain/.org.jooq.*— jOOQ-типы остаются в адаптере.com.fasterxml.jackson.*— Jackson-аннотации не вdomain/.org.springframework.web.*— HTTP/REST-аннотации тоже.
// ПЛОХО — JPA-аннотации в domain
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue
private Long id;
@Column(name = "status")
private String status;
}
// ХОРОШО — pure domain
public final class Order extends AggregateRoot<OrderId> {
private final OrderId id;
private OrderStatus status;
// ...
}
Маппинг доменного Order ↔ jOOQ-record делается в OrderDomainRecordMapper (в adapter/out/postgres/). Подробно — в Маппинг record ↔ domain.
Spring в core/ — на каких уровнях
Уровень зрелости 1–2 (см. Hexagonal: когда переходить):
@ComponentнаUseCaseHandler-ах вcore/<bc>/usecase/command/— ок. Spring должен их находить через component-scan.@ComponentнаRepository-реализации — её вcore/нет (реализация вadapter/out/...).@RequiredArgsConstructor(Lombok) для DI — ок.
Уровень зрелости 3 (полный Hexagonal в multi-module gradle):
core/— отдельный модуль без Spring зависимостей.@Componentуходит. Регистрация бинов — через явный@Configurationвbootstrap/, который импортирует pure-Java классы изcore/и оборачивает в@Bean.- ArchUnit-тест на отсутствие
org.springframework.*вcore/— required CI check (см. ArchUnit-тесты Hexagonal).
Граница — выбор уровня. Большинство сервисов в начале живут на Уровне 2 — @Component в core/ допустимо. Когда сервис вырастает в multi-module, core/ чистится от Spring.
usecase/ — соседняя половина BC
usecase/ живёт рядом с domain/, в том же BC. Здесь — команды (мутирующие операции), запросы (read-операции) и их хендлеры:
core/order/usecase/
command/
CreateOrder.java # record CreateOrder(CustomerId customerId, List<...> items) implements UseCaseCommand
CreateOrderHandler.java # @Component implements UseCaseHandler<CreateOrder, OrderId>
ConfirmOrder.java
ConfirmOrderHandler.java
query/
GetOrder.java # record GetOrder(OrderId id) implements UseCaseQuery
GetOrderHandler.java
FindActiveOrders.java
FindActiveOrdersHandler.java
Команды и хендлеры — это Application Service уровень. Они оркеструют: загружают агрегаты, вызывают методы, сохраняют. Бизнес-правила живут в domain/, оркестрация — в usecase/.
Подробно про команды/хендлеры — в Use Case Pattern Style Guide. Про CQRS-разделение query и command — в CQRS Style Guide.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Верхнеуровневые entity/, service/, repository/ (группировка по типу) | R-MOD-1 | Группировка по Bounded Context: core/order/, core/customer/ |
@Entity, @Table, JPA-аннотации в domain/ | R-MOD-2 | Pure domain + маппинг в адаптере |
org.jooq.* в core/<bc>/domain/... | R-MOD-2 | jOOQ только в adapter/out/postgres/ |
@RestController, HTTP-аннотации в core/ | R-MOD-2 | Controller в adapter/in/rest/ |
Импорт класса другого BC напрямую (core.order.usecase импортирует core.customer.domain.aggregate.Customer) | R-AGG-5 + R-MOD-1 | Между BC — по ID, либо через published events |
Куда дальше
- DDD Tactical → раздел 9. Module — нормативные формулировки
R-MOD-*. - Hexagonal Architecture → Структура модулей — на multi-module Gradle.
- Hexagonal Architecture → Core слой — подробно о допустимых зависимостях.
- Hexagonal Architecture → ArchUnit-тесты — required CI check.
- Use Case Pattern Style Guide — как устроены
usecase/command/иusecase/query/. - Java Style Guide — Lombok-аннотации и
@RequiredArgsConstructorдля DI.