В гексагональном приложении код разбит на несколько модулей: core/ с бизнес-логикой, persistence/ с базой данных, адаптеры для HTTP, Kafka и других систем. Кто-то должен собрать всё это вместе и запустить — этим занимается bootstrap/.
Зачем нужен отдельный bootstrap-модуль
Без выделенного bootstrap-модуля точка входа живёт где придётся — в core/ или в одном из адаптеров. Это быстро ломает структуру: модуль с бизнес-логикой начинает тянуть Spring Boot, конфигурации размываются по нескольким местам, становится непонятно, что откуда зависит.
bootstrap/ — это composition root: место, в котором собирается приложение. Его роль строго ограничена:
- объявить точку входа (
main); - собрать Spring-контекст из всех модулей;
- владеть конфигурационными файлами (
application.yml); - содержать
Dockerfileи инфраструктурные скрипты.
Никакой бизнес-логики, никаких контроллеров — только сборка.
Что лежит в bootstrap/
Типичная структура:
bootstrap/
├── src/main/java/<pkg>/bootstrap/
│ ├── <App>Application.java # @SpringBootApplication + main()
│ └── config/ # @Configuration-классы для wiring
│ ├── ClockConfig.java
│ ├── ObjectMapperConfig.java
│ └── SecurityConfig.java
├── src/main/resources/
│ ├── application.yml # общий конфиг
│ ├── application-local.yml # локальный профиль
│ ├── application-production.yml
│ └── logback-spring.xml
├── Dockerfile
└── docker-compose.yml
Минимальная точка входа:
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
В @Configuration-классах живут только бины, которым нужна явная настройка:
ClockиUuidProvider— production-реализации интерфейсов изcore/;ObjectMapperс кастомными модулями;RestClient-бины, если их сборка не делается внутри адаптеров.
Большинство бинов поднимается автоматически через component scan — @Component-классы в адаптерах Spring находит сам.
bootstrap/ зависит от всех модулей
В 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"))
// 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")
runtimeOnly("org.postgresql:postgresql")
}
И при этом никто не зависит от bootstrap/ — в core/, persistence/ и адаптерах нет project(":bootstrap"). Стрелки зависимостей идут только в одну сторону: bootstrap → core ← adapters. bootstrap/ — закрывающий узел, в котором заканчиваются все связи.
Если core/ или адаптер начнёт зависеть от bootstrap/, получится циклическая зависимость — Gradle откажется собирать проект.
Как Spring находит бины из всех модулей
@SpringBootApplication запускает сканирование пакетов начиная с пакета своего класса. Есть три варианта.
Вариант 1 — общий корневой пакет. Если все модули лежат под одним корнем, Spring сам найдёт все @Component-классы:
package ru.example.order; // корневой пакет
@SpringBootApplication
public class OrderServiceApplication { /* ... */ }
При условии, что все модули используют пакеты ru.example.order.*.
Вариант 2 — явный список пакетов. Когда структура пакетов не позволяет использовать общий корень:
@SpringBootApplication(scanBasePackages = {
"ru.example.order.core",
"ru.example.order.persistence",
"ru.example.order.userapi",
"ru.example.order.sberout",
})
public class OrderServiceApplication { /* ... */ }
Вариант 3 — явный импорт конфигурации. Каждый адаптер экспортирует свой @Configuration-класс, bootstrap его импортирует:
@SpringBootApplication
@Import({PersistenceConfig.class, SberOutAdapterConfig.class, UserApiInAdapterConfig.class})
public class OrderServiceApplication { /* ... */ }
Этот вариант чище в смысле явных контрактов между модулями, но требует больше кода. На практике чаще используют вариант 1 или 2.
Профили и application.yml
Все профили конфигурации живут в bootstrap/src/main/resources/. core/ и адаптеры их не видят и не контролируют.
# application.yml — общий для всех профилей
spring:
application:
name: order-service
# 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}
Разделение на профили позволяет запускать приложение локально без правки основного конфига — достаточно передать -Dspring.profiles.active=local.
Частые ошибки
Контроллеры и логика в bootstrap
Соблазн быстро добавить обработчик прямо в bootstrap понятен, но это разрушает архитектуру:
// Неправильно — контроллер в bootstrap
package ru.example.order.bootstrap;
@RestController
public class OrderController {
@PostMapping("/orders")
public OrderJson createOrder(@RequestBody CreateOrderRequest req) { ... }
}
Проблема в том, что bootstrap/ должен быть тонким — только сборка. Как только там появляется логика:
- её сложно перенести в нужный модуль без рефакторинга;
- тест на такой контроллер вынужден поднимать весь контекст, хотя достаточно было бы легковесного
@WebMvcTest.
Правило простое: контроллеры — в *-in-adapter/, бизнес-логика — в core/, bootstrap/ — только сборка.
@SpringBootApplication не на своём месте
Если @SpringBootApplication оказывается в core/ или в адаптере, возникают конкретные проблемы:
// Неправильно — @SpringBootApplication в core
package ru.example.order.core;
@SpringBootApplication
public class CoreApplication { /* ... */ }
Во-первых, core/ начинает тянуть всю Spring Boot инфраструктуру — теряется возможность использовать ядро без фреймворка. Во-вторых, когда в проекте два @SpringBootApplication (в core/ и в bootstrap/), непонятно, что запускать. В-третьих, оба класса запускают сканирование пакетов — они могут конфликтовать.
@SpringBootApplication должен быть ровно один, строго в bootstrap/.
Коротко
bootstrap/— composition root: точка входа, Spring-контекст,application.yml,Dockerfile.bootstrap/зависит от всех остальных модулей; никто не зависит отbootstrap/.- Spring-бины из адаптеров подхватываются через component scan или явный
@Import. - Все профили (
local,production) хранятся вbootstrap/src/main/resources/. - Контроллеры и бизнес-логика в
bootstrap/— ошибка: модуль становится нетонким и его сложно тестировать. @SpringBootApplication— ровно один, только вbootstrap/.
Что почитать дальше
- Структура модулей в Hexagonal — как core, адаптеры и bootstrap связаны между собой.
- Архитектурные тесты — как проверить правильность зависимостей между модулями автоматически.
- Spring DI/IoC и жизненный цикл бина — как Spring создаёт и связывает объекты внутри контекста.