← назад к разделу

В гексагональном приложении код разбит на несколько модулей: 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 инфраструктуру — теряется возможность использовать ядро без фреймворка. Во-вторых, когда в проекте два @SpringBootApplicationcore/ и в 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 создаёт и связывает объекты внутри контекста.