Опирается на правила: R-MOD-1R-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 делается в OrderDomainRecordMapperadapter/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-2Pure domain + маппинг в адаптере
org.jooq.* в core/<bc>/domain/...R-MOD-2jOOQ только в adapter/out/postgres/
@RestController, HTTP-аннотации в core/R-MOD-2Controller в 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.