Опирается на правила: R-HEX-BOOT-1R-HEX-BOOT-3 и R-HEX-BOOT-X1R-HEX-BOOT-X2 из Hexagonal Style Guide → раздел 7. Bootstrap / composition root.

Важно знать

  • bootstrap/ — composition root: @SpringBootApplication + application.yml + @Configuration-классы + Dockerfile.
  • bootstrap/build.gradle.kts зависит от всех модулей: core/, persistence/, всех *-in-adapter/, всех *-out-adapter/.
  • Никто не зависит от bootstrap/ — это закрывающий узел.
  • @SpringBootApplication(scanBasePackages = ...) покрывает все адаптеры либо через component scan, либо через explicit @Import.
  • Бизнес-логика и REST-контроллеры в bootstrap/ — запрещены. Только композиция и configs.
  • @SpringBootApplication в core/ или адаптере — запрещён. Только в bootstrap/, иначе невозможно собрать сервис из частей.

bootstrap/ — это маленький, но критически важный модуль. Все остальные — core/, адаптеры — это «кирпичи». bootstrap/ — это «дом», который собирается из этих кирпичей. Он содержит точку входа (main), конфигурацию Spring, application.yml, Dockerfile. И ничего больше. Никакой бизнес-логики, никаких контроллеров — всё это в соответствующих модулях. Раскрытие правил R-HEX-BOOT-* ниже.

Что лежит в bootstrap/

R-HEX-BOOT-1: точный состав модуля.

bootstrap/
├── src/main/java/<pkg>/bootstrap/
│   ├── <App>Application.java          # @SpringBootApplication + main()
│   ├── config/                        # @Configuration-классы для wiring
│   │   ├── ClockConfig.java
│   │   ├── ObjectMapperConfig.java
│   │   └── SecurityConfig.java        # (если не разнесён по in-adapter)
│   └── ...
├── src/main/resources/
│   ├── application.yml                # общий config
│   ├── application-local.yml          # локальный профиль
│   ├── application-integration-test.yml
│   ├── application-production.yml
│   └── logback-spring.xml
├── Dockerfile
├── docker-compose.yml                  # для local-разработки
└── helm/                               # Helm chart (если используется)

Минимальная точка входа:

@SpringBootApplication
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

В @Configuration-классах живут только бины, которые требуют явного провода:

  • Production-бины для interfaces Clock, UuidProvider (см. Bootstrap design).
  • ObjectMapper с custom-настройками (если стандартного Spring Boot mapper'а не хватает).
  • Custom DefaultConfigurationCustomizer для jOOQ (см. DSLContext).
  • RestClient-bean'ы (если их wiring не делается per-system в out-adapter'ах).

Большинство бинов поднимется автоматически через component scan — @Component-аннотированные классы в адаптерах попадут в ApplicationContext без явного wiring.

bootstrap/ зависит от всех

R-HEX-BOOT-2: gradle-зависимости.

// bootstrap/build.gradle.kts
dependencies {
    implementation(project(":core"))

    implementation(project(":persistence"))
    implementation(project(":user-api-in-adapter"))
    implementation(project(":admin-api-in-adapter"))
    implementation(project(":kafka-in-adapter"))
    implementation(project(":sber-out-adapter"))
    implementation(project(":sms-out-adapter"))
    implementation(project(":kafka-out-adapter"))
    implementation(project(":scheduler-out-adapter"))

    // Spring Boot стартеры — только тут
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-actuator")
    implementation("org.springframework.boot:spring-boot-starter-jooq")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
    implementation("org.springframework.kafka:spring-kafka")

    // Liquibase
    implementation("org.liquibase:liquibase-core")
    runtimeOnly("org.postgresql:postgresql")
}

И никто не зависит от bootstrap/. В core/build.gradle.kts, persistence/build.gradle.kts, и любом другом модуле — нет project(":bootstrap"). Это критично:

  • bootstrap/ — sink-модуль, в нём заканчиваются все стрелки зависимостей.
  • Если бы core/ зависел от bootstrap/, циклическая зависимость в gradle сломает сборку.
  • Если бы adapter зависел от bootstrap — нарушение стрелки bootstrap → core ← adapters (см. Module structure → R-HEX-MOD-X2).

Component scan покрывает адаптеры

R-HEX-BOOT-3: два способа сделать бины из адаптеров видимыми для Spring.

Вариант 1 — default component scan по корневому пакету. @SpringBootApplication сканирует пакет своего класса и все суб-пакеты:

package ru.example.order;                            // ← корневой пакет приложения

@SpringBootApplication
public class OrderServiceApplication { /* ... */ }

Если все модули лежат в пакетах под ru.example.order.*:

core/.../ru/example/order/core/...
persistence/.../ru/example/order/persistence/...
user-api-in-adapter/.../ru/example/order/userapi/...
sber-out-adapter/.../ru/example/order/sberout/...

Spring увидит все @Component/@Service/@RestController-классы автоматически.

Вариант 2 — explicit scanBasePackages если структура не позволяет default:

@SpringBootApplication(scanBasePackages = {
    "ru.example.order.core",
    "ru.example.order.persistence",
    "ru.example.order.userapi",
    "ru.example.order.sberout",
    // ...
})
public class OrderServiceApplication { /* ... */ }

Вариант 3 — @Import-классов конфигурации. Каждый адаптер экспортирует свой @Configuration-класс, bootstrap явно импортирует:

@SpringBootApplication
@Import({PersistenceConfig.class, SberOutAdapterConfig.class, UserApiInAdapterConfig.class})
public class OrderServiceApplication { /* ... */ }

Это чище в смысле «явного контракта между модулями», но требует больше boilerplate. На практике — variant 1 или 2.

Профили в application.yml

bootstrap/ владеет всеми профилями:

# application.yml — общий для всех профилей
spring:
  application:
    name: order-service
  jpa:
    open-in-view: false

# application-local.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/orders
    username: orders
    password: orders
sber:
  api-url: https://sber-sandbox.example.com

# application-production.yml
spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USER}
    password: ${DB_PASSWORD}
sber:
  api-url: ${SBER_API_URL}

См. Spring Bootstrap Style Guide — детальные правила раскладки профилей.

Что запрещено

R-HEX-BOOT-X1: бизнес-логика или REST-контроллеры в bootstrap/.

// ПЛОХО
package ru.example.order.bootstrap;

@RestController                                       // ← контроллер в bootstrap
public class OrderController {
    @PostMapping("/orders")
    public OrderJson createOrder(@RequestBody CreateOrderRequest req) {
        // ...
    }
}

Что не так:

  • Размывается ответственность. bootstrap/ должен быть «сборка из частей», как только в нём появляется логика — он перестаёт быть тонким composition root.
  • Невозможно перенести. Логика в bootstrap не уезжает в *-in-adapter/-модуль без рефакторинга — она впаяна в Spring-application.
  • Тестируется как полконтекста. Тест на controller — это @SpringBootTest (потому что класс в bootstrap), а должен быть @WebMvcTest на in-adapter-модуле.

Controller — в user-api-in-adapter или admin-api-in-adapter. Логика — в core/. bootstrap/ — только composition.

R-HEX-BOOT-X2: @SpringBootApplication в core/ или адаптере.

// ПЛОХО
package ru.example.order.core;

@SpringBootApplication                                // ← в core?!
public class CoreApplication { /* ... */ }

Что не так:

  • Невозможно собрать сервис из частей. Когда @SpringBootApplication в core/, любой сервис на основе этого core тянет всю Spring-инфраструктуру. Идея «core можно использовать в Lambda» рушится.
  • Дублирование конфигурации. Один Application-класс в core, второй в bootstrap = два контекста, два main()-метода — что запускать?
  • @SpringBootApplication несёт component scan. Если он стартует из core-пакета, его scan может перекрыть scan из bootstrap — конфликт.

@SpringBootApplication строго в bootstrap/. Один. Везде.

Куда дальше