Перейти к содержанию
V1 V2

Как построить собственный первоклассный модуль Kora

15 августа 2026

Утверждение фреймворка Kora о том, что он создан для расширения, интереснее общего заявления о том, что разработчики могут регистрировать кастомные компоненты.

Почти любой фреймворк внедрения зависимостей может поместить сторонний клиент в контейнер. Если вам нужен только клиент Elasticsearch, соединение NATS, клиент MinIO или объект внутренней библиотеки, базовая проблема проста: сконструировать его один раз и внедрить.

Первоклассный модуль фреймворка — гораздо более высокая планка.

Первоклассный модуль Kora должен заставлять внешнюю технологию вести себя так, будто она естественно принадлежит фреймворку. Он должен участвовать в типизированной конфигурации, внедрении зависимостей на этапе компиляции, жизненном цикле, корректном завершении работы, телеметрии, тестировании, замене компонентов и той же явной модели приложения, которую используют встроенные интеграции Kora.

Основной паттерн:

сторонняя библиотека
        ↓
тонкий интеграционный модуль Kora
        ↓
типизированная конфигурация
жизненный цикл
телеметрия
фабрики DI
опциональные пробы
        ↓
граф приложения
        ↓
бизнес-компоненты

Модуль не должен пытаться стереть внешнюю технологию. Если у Elasticsearch уже есть хороший Java API, разработчики приложения всё равно должны мыслить концепциями Elasticsearch. Если NATS раскрывает subjects, подписки, JetStream и подтверждения, эти концепции должны оставаться видимыми. Если MinIO — S3-совместимое объектное хранилище, семантика объектного хранилища не должна заменяться произвольным DSL фреймворка.

Работа Kora — предоставить производственный клей:

библиотека остаётся библиотекой
Java/Kotlin остаётся Java/Kotlin
Kora поставляет конфигурацию, жизненный цикл, телеметрию и связку

Это правильный способ понимать модель расширяемости фреймворка.

Модуль — это больше, чем фабричный метод

Начнём с вымышленной библиотеки:

public final class SearchClient {
    public SearchClient(String endpoint, String apiKey) {
        // ...
    }

    public SearchResult search(SearchRequest request) {
        // ...
    }

    public void close() {
        // ...
    }
}
class SearchClient(endpoint: String, apiKey: String) {

    fun search(request: SearchRequest): SearchResult {
        // ...
    }

    fun close() {
        // ...
    }
}

Наивный модуль мог бы быть таким:

@Module
public interface SearchModule {

    default SearchClient searchClient() {
        return new SearchClient(
            "https://search.internal",
            "secret"
        );
    }
}
@Module
interface SearchModule {

    fun searchClient(): SearchClient {
        return SearchClient(
            "https://search.internal",
            "secret"
        )
    }
}

Технически это интегрирует клиент с DI. Операционно это слабо. Эндпоинт и секрет захардкожены, жизненный цикл не управляется, телеметрии не существует, несколько экземпляров невозможны, тестирование требует ручной замены реализации, и нет явного производственного контракта.

Первоклассная интеграция должна решать реальный жизненный цикл технологии, а не просто конструирование.

Полезный базовый уровень:

1. Конфигурация
2. Внедрение зависимостей
3. Жизненный цикл
4. Телеметрия
5. Тестируемость

В зависимости от технологии производственному модулю могут также понадобиться:

6. Пробы
7. Поддержка нескольких экземпляров
8. Корректное завершение работы
9. Хуки отказоустойчивости
10. Поддержка native-image
11. Утилиты тестирования
12. Сгенерированные адаптеры

Не каждой библиотеке нужна каждая функция. Маленькому форматтеру без состояния может понадобиться только фабрика. Интеграции потребителя NATS нужно значительно больше. Модуль должен моделировать реальную операционную природу технологии.

Модули Kora принадлежат графу этапа компиляции

Граф зависимостей Kora в значительной степени валидируется во время компиляции. Поэтому модуль — не просто хук регистрации времени выполнения. Он становится частью той же архитектуры этапа компиляции, что и контроллеры, репозитории, клиенты, компоненты телеметрии и другие зависимости приложения.

Внутри единицы компиляции приложения интерфейс, аннотированный @Module, может предоставлять фабричные методы:

@Module
public interface SearchModule {

    default SearchService searchService(SearchClient client) {
        return new SearchService(client);
    }
}
@Module
interface SearchModule {

    fun searchService(client: SearchClient): SearchService {
        return SearchService(client)
    }
}

Параметры — это зависимости графа. Если у SearchClient нет провайдера или существует несколько неоднозначных провайдеров, Kora может отклонить приложение до запуска.

Это важно для авторов расширений, потому что им не нужно строить собственный контейнер или реестр времени выполнения. Они описывают компоненты и отношения; Kora компилирует структуру.

Внешние модули подключаются явно

Для переиспользуемых модулей, поставляемых в отдельном артефакте, модель Kora намеренно явная. Приложение включает внешний модуль через свой интерфейс @KoraApp, а не через сканирование classpath.

Концептуально:

public interface SearchClientModule {
    // переиспользуемые интеграционные фабрики
}
interface SearchClientModule {
    // переиспользуемые интеграционные фабрики
}

а в сервисе:

@KoraApp
public interface Application extends SearchClientModule {
}
@KoraApp
interface Application : SearchClientModule

Это различие важно.

Добавление JAR в classpath не автоматически означает:

инициализировать его клиент
запустить его потоки
подключиться к его удалённой системе
зарегистрировать все его компоненты

Приложение явно говорит:

Я хочу этот модуль в своём графе.

Это делает интерфейс @KoraApp архитектурным манифестом.

Сервис может читаться так:

@KoraApp
public interface Application
    extends JsonModule,
    JdbcDatabaseModule,
    SearchClientModule,
    NatsModule,
    MinioModule {
}
@KoraApp
interface Application :
    JsonModule,
    JdbcDatabaseModule,
    SearchClientModule,
    NatsModule,
    MinioModule

Инженер может немедленно увидеть, какие семейства инфраструктуры включены. Компилятор видит то же самое. нейро-агент видит то же самое.

Это сильнее скрытой авто-конфигурации.

Начните с тонкой интеграционной границы

Первый вопрос дизайна — не «Какую абстракцию фреймворка я могу изобрести?»

Он такой:

Что нативная библиотека уже делает хорошо, и какую производственную интеграцию Kora должна добавить вокруг неё?

Предположим, внешняя библиотека уже раскрывает хороший SearchClient. Модуль может просто раскрыть этот клиент напрямую:

сервис приложения
      ↓
SearchClient
      ↓
сторонняя библиотека

Не добавляйте сразу:

KoraSearchTemplate
KoraSearchOperations
KoraSearchRepository
KoraSearchGateway

если только эти абстракции не решают повторяющиеся проблемы.

Интеграция фреймворка и абстракция технологии — разные задачи.

Интеграция фреймворка отвечает:

Как это настраивается?
Кто это создаёт?
Кто это закрывает?
Как это наблюдается?
Как это заменяется в тестах?
Как сосуществуют несколько экземпляров?

Абстракция технологии отвечает:

Должен ли код приложения вообще видеть нативную библиотеку?
Должны ли мы раскрывать доменные репозитории?
Должны ли мы изолировать API вендора?

Первое обычно необходимо.

Второе опционально.

Первоклассный модуль не должен навязывать большой специфичный для фреймворка API, когда нативная библиотека уже хороша.

Шаг 1: Моделируйте конфигурацию как тип

Производственная интеграция обычно должна начинаться с типизированной конфигурации.

Например:

@ConfigMapper
public interface SearchConfig {

    URI endpoint();

    String apiKey();

    default Duration connectTimeout() {
        return Duration.ofSeconds(2);
    }

    default Duration requestTimeout() {
        return Duration.ofSeconds(5);
    }

    default int maxConnections() {
        return 50;
    }
}
@ConfigMapper
interface SearchConfig {

    fun endpoint(): URI

    fun apiKey(): String

    fun connectTimeout(): Duration {
        return Duration.ofSeconds(2)
    }

    fun requestTimeout(): Duration {
        return Duration.ofSeconds(5)
    }

    fun maxConnections(): Int {
        return 50
    }
}

Конфигурация может выглядеть так:

search {
    endpoint = "https://search.internal"
    apiKey = ${SEARCH_API_KEY}
    connectTimeout = "2s"
    requestTimeout = "5s"
    maxConnections = 50
}
search:
    endpoint: "https://search.internal"
    apiKey: "${SEARCH_API_KEY}"
    connectTimeout: 2s
    requestTimeout: 5s
    maxConnections: 50

Интерфейс конфигурации описывает форму и семантику конфигурации модуля.

Для переиспользуемого кода библиотеки полезно держать сам тип независимым от одного жёсткого пути конфигурации. Фабрика модуля может выбирать, откуда маппится эта форма.

Это допускает переиспользование, например:

search.primary
search.analytics

без копирования одной и той же модели конфигурации.

Конфигурация библиотеки должна быть независимой от пути

Есть два отдельных контракта:

форма конфигурации
расположение конфигурации

Тип отвечает:

Какие значения существуют?
Какие обязательны?
Каковы их типы Java?
Какие значения по умолчанию применяются?

Фабрика модуля отвечает:

Где это приложение их хранит?

Это разделение особенно полезно, когда общий модуль потребляется разными проектами или несколькими именованными экземплярами.

Оно также предотвращает хардкод иерархии конфигурации одной организации в переиспользуемой интеграции.

Избегайте сырой конфигурации глубоко в модуле

Можно внедрять сырую конфигурацию и снова и снова писать:

config.get("search.endpoint").asString();
config.get("search.endpoint").asString()

но это ослабляет модуль.

Строковые пути расползаются по коду конструирования. Значения по умолчанию и валидация разбрасываются. Конфигурация становится труднее тестируемой. Поведение обновления может непреднамеренно затрагивать большую часть графа.

Предпочитайте такую структуру:

сырой Config
   ↓
ConfigValueMapper<SearchConfig>
   ↓
SearchConfig
   ↓
все остальные компоненты модуля

Сырая конфигурация обычно должна оставаться на границе.

Типизированная конфигурация становится реальным контрактом модуля.

Конфигурация — это публичный API

Как только модуль становится общим, его конфигурация так же важна, как его Java API.

Изменение:

search:
    endpoint:

на:

search:
    url:

может быть ломающим изменением развёртывания, даже если каждый класс Java всё ещё компилируется.

Значения по умолчанию тоже важны. Изменение таймаута по умолчанию с пяти секунд на шестьдесят меняет производственное поведение.

Первоклассный модуль должен документировать и версионировать:

имена свойств
типы
единицы
значения по умолчанию
обязательные значения
значения, чувствительные к безопасности
семантическое значение

Конфигурация — не деталь реализации.

Значения по умолчанию должны быть безопасными для продакшена

Избегайте удобных, но опасных значений по умолчанию, таких как:

бесконечный таймаут
неограниченный пул
отключённая проверка TLS
автоматическое создание разрушительных ресурсов
неограниченный повтор

Предпочитайте ограниченные, явные значения по умолчанию:

конечные таймауты
ограниченные соединения
безопасный TLS
консервативный повтор
предсказуемое завершение

Переиспользуемый модуль централизует производственное поведение во многих сервисах. Плохие значения по умолчанию масштабируются так же эффективно, как и хорошие.

Шаг 2: Сконструируйте клиент через DI

Как только конфигурация существует, создайте клиент, используя обычные зависимости графа:

public interface SearchClientModule {

    default SearchClient searchClient(SearchConfig config) {
        return new SearchClient(
            config.endpoint(),
            config.apiKey(),
            config.connectTimeout(),
            config.requestTimeout()
        );
    }
}
interface SearchClientModule {

    fun searchClient(config: SearchConfig): SearchClient {
        return SearchClient(
            config.endpoint(),
            config.apiKey(),
            config.connectTimeout(),
            config.requestTimeout()
        )
    }
}

Бизнес-компоненты теперь зависят от обычного типа:

@Component
public final class CatalogSearchService {

    private final SearchClient search;

    public CatalogSearchService(SearchClient search) {
        this.search = search;
    }
}
@Component
class CatalogSearchService(private val search: SearchClient)

Нет статического реестра.

Нет сервис-локатора.

Нет поиска во время выполнения.

Граф зависимостей говорит ровно то, что требуется приложению.

Граф должен раскрывать реальные инфраструктурные зависимости

Предположим, интеграции также нужны:

CredentialsProvider
SearchTelemetry
конфигурация TLS
транспорт HTTP

Сделайте эти зависимости видимыми:

default SearchClient searchClient(
    SearchConfig config,
    CredentialsProvider credentials,
    SearchTelemetry telemetry,
    SearchTransport transport
) {
    // ...
}
fun searchClient(
    config: SearchConfig,
    credentials: CredentialsProvider,
    telemetry: SearchTelemetry,
    transport: SearchTransport
): SearchClient {
    // ...
}

Это даёт Kora полезную информацию.

Граф может валидировать полноту.

Тесты могут заменять отдельные части.

Порядок жизненного цикла может следовать реальным отношениям зависимостей.

Сгенерированная связка становится читаемой.

Скрытые глобальные поиски уничтожают эти преимущества.

Внедрение зависимостей на этапе компиляции — страховочная сеть автора модуля

Предположим, ваш модуль требует:

SearchConfig
SearchTelemetry
CredentialsProvider

Если приложение забывает провайдер учётных данных, это должно стать ошибкой построения графа, а не поздним сюрпризом времени выполнения.

Это одна из самых сильных причин реализовывать инфраструктуру как реальные модули Kora.

Автору модуля не нужна кастомная валидация запуска для базового существования зависимостей и неоднозначности. Kora уже предоставляет эту структурную проверку.

Шаг 3: Относитесь к жизненному циклу как к части API

Инфраструктурные клиенты часто владеют ресурсами:

TCP-соединения
потоки event-loop
пулы воркеров
подписки
heartbeat'ы
буферы
фоновые задачи обновления

Создание объекта — лишь половина проблемы.

Интеграция должна ответить:

Когда он готов?
Кто его запускает?
Кто его останавливает?
Кто закрывает сетевые ресурсы?
Что происходит при частичном сбое запуска?

У Kora есть явная семантика жизненного цикла компонентов.

Компонент может реализовывать:

public interface Lifecycle {
    void init() throws Exception;

    void release() throws Exception;
}
interface Lifecycle {
    fun init()

    fun release()
}

Для внешних типов, которые не могут реализовывать интерфейсы Kora, фабрика может прикрепить жизненный цикл через обёртку.

Это ровно то, для чего существует интеграционный слой: сохранение внешнего API при подключении жизненного цикла его ресурсов к графу приложения.

Используйте AutoCloseable, когда его достаточно

Если нативный клиент уже реализует AutoCloseable, не изобретайте параллельную абстракцию жизненного цикла только ради согласованности.

Граф может закрывать принадлежащие ресурсы во время release.

Предпочитайте нативные контракты, когда они корректно представляют владение.

Хорошая интеграция Kora остаётся близко к базовой технологии.

Используйте жизненный цикл, когда запуск имеет реальное значение

Интеграции NATS может понадобиться:

подключиться
зарегистрировать подписки
инициализировать потребителей JetStream
ждать готовности диспетчера

Клиенту MinIO может не понадобиться значимая инициализация сверх конструирования.

Транспорту Elasticsearch может понадобиться создать ресурсы соединения, но не выполнять блокирующий вызов здоровья кластера.

Не заставляйте каждый модуль выполнять сетевую валидацию при запуске только потому, что жизненный цикл существует.

Каждый обязательный удалённый вызов при запуске увеличивает время до готовности и делает развёртывания более чувствительными к внешним сбоям.

Модуль должен тщательно различать:

объект создан
ресурс инициализирован
приложение готово

LifecycleWrapper полезен для сторонних объектов

Фабрика может концептуально возвращать обёрнутый клиент:

default Wrapped<SearchClient> searchClient(SearchConfig config) {
    var client = createSearchClient(config);

    return new LifecycleWrapper<>(
        client,
        value -> {
            // опциональная инициализация
        },
        SearchClient::close
    );
}
fun searchClient(config: SearchConfig): Wrapped<SearchClient> {
    val client = createSearchClient(config)

    return LifecycleWrapper(
        client,
        {
            // опциональная инициализация
        },
        SearchClient::close
    )
}

Остальная часть приложения по-прежнему внедряет SearchClient.

Жизненный цикл остаётся задачей интеграции.

Это сильный паттерн для адаптации библиотек, которые ничего не знают о Kora.

Обратное освобождение зависимостей ценно

Предположим:

SearchIndexer
зависит от
SearchClient

Во время завершения работы зависимый компонент должен остановиться первым, затем может закрыться клиент.

Kora освобождает компоненты графа в обратном порядке зависимостей.

Это значит, что корректное моделирование графа естественно производит разумное завершение ресурсов.

Это ещё одна причина, почему скрытые зависимости вредны: граф не может упорядочить ресурсы, которых не видит.

Корректное завершение работы определяет качество интеграции

Обмен сообщениями делает это особенно очевидным.

Модуль потребителя NATS должен думать о:

прекращении приёма новых сообщений
завершении или отмене обработчиков в полёте
сбросе публикаций
сливе подписок
закрытии соединения

Точное поведение зависит от NATS и политики приложения.

Но просто немедленно закрыть сокет во время каждой раскатки недостаточно для серьёзной интеграции.

Клиент может быть полностью корректным в установившемся режиме и всё же быть операционно плохим, если каждое развёртывание теряет работу.

Первоклассные модули интегрируют семантику завершения.

Избегайте ненужной работы при запуске

Kora инициализирует независимые компоненты графа параллельно, где возможно.

Кастомный модуль может легко стереть это преимущество, выполняя несвязанную последовательную работу в одной фабрике:

подключиться
пинговать кластер
создать ресурсы
скачать схему
прогреть кеш
загрузить метаданные

Отделяйте обязательную инициализацию от опционального прогрева.

Не выполняйте работу развёртывания/плоскости управления во время обычного запуска приложения, если она не обязательна.

Производительность запуска фреймворка зависит и от авторов модулей.

Шаг 4: Дайте модулю контракт телеметрии

Производственный модуль должен ответить:

Как оператор наблюдает эту технологию?

Как минимум подумайте о:

логировании
метриках
трассировке

Полезный дизайн — специфичная для модуля абстракция телеметрии:

public interface SearchTelemetry {

    Context create(SearchOperation operation);

    interface Context {
        void close(Throwable error);
    }
}
interface SearchTelemetry {

    fun create(operation: SearchOperation): Context

    interface Context {
        fun close(error: Throwable?)
    }
}

Затем каждая операция следует одному жизненному циклу:

начать наблюдение
  ↓
выполнить нативную операцию
  ↓
закрыть с успехом или сбоем

Это зеркалит более широкую архитектуру телеметрии Kora: контекст, специфичный для операции, вокруг реальной работы.

Один контекст наблюдения лучше трёх независимых обёрток

Слабая реализация может вручную делать:

logger.start
timer.start
span.start
операция
logger.end
timer.stop
span.end

в каждом методе.

Лучшая структура:

SearchTelemetry
  ├── логирование
  ├── метрики
  └── трассировка

и один контекст операции обрабатывает симметрию start/end.

Это централизует семантику успеха/сбоя и делает no-op телеметрию дешёвой, когда она отключена.

Телеметрия должна описывать нативную технологию

Полезные измерения в стиле Elasticsearch могут включать:

operation=search
index=products
result=success

Для NATS:

operation=publish
subject=orders.created

Для MinIO:

operation=put_object
bucket=assets

Не прячьте это за специфичным для фреймворка словарём, если операторы уже понимают технологию.

В то же время кардинальность должна контролироваться.

Ключ объекта — обычно ужасная метка метрики.

Subject NATS может быть безопасным, если ограничен, и опасным, если генерируется динамически.

Индекс Elasticsearch может быть ограничен в одной архитектуре и неограничен в другой.

Знания технологии всё равно требуются.

Метрикам нужны ограниченные измерения

Метрики — инструменты агрегации.

Хорошие измерения часто включают:

операцию
ограниченное имя ресурса
класс результата
категорию ошибки

Плохие измерения часто включают:

идентификатор запроса
идентификатор пользователя
ключ объекта
сырой запрос
полное сообщение исключения

Первоклассный модуль должен выбирать безопасные значения по умолчанию, чтобы потребляющие сервисы не открывали правила кардинальности заново независимо.

Трассировки могут нести более богатый контекст

Трассировка обычно может терпеть более богатые метаданные уровня запроса, чем метрики.

Span поиска может включать:

system=elasticsearch
operation=search
index=products
result.count=20

не добавляя полное тело запроса.

Span публикации NATS может включать subject и размер полезной нагрузки без копирования содержимого сообщения.

Span загрузки в MinIO может включать bucket и размер, избегая чувствительных ключей.

Телеметрия должна быть полезной, не становясь утечкой данных.

Логи должны фиксировать исключительные состояния, а не каждый успех

Полезные логи модуля включают:

переподключение NATS началось
переподключение NATS завершено
узел Elasticsearch помечен недоступным
многокомпонентная загрузка MinIO прервана
обновление учётных данных не удалось

Модулю обычно не нужен INFO-лог для каждого успешного запроса, если метрики и трассировка уже покрывают установившееся поведение.

Структурированное логирование должно объяснять необычные решения и переходы состояний.

Переиспользуйте существующий стек наблюдаемости Kora

Кастомная интеграция обычно должна зависеть от существующей инфраструктуры метрик, трассировки и логирования приложения, а не изобретать ещё один стек экспортёров.

Это даёт одному сервису:

один MeterRegistry
одну настройку OpenTelemetry
одну конвенцию логирования

вместо другой системы мониторинга для каждой библиотеки.

Именно это заставляет кастомный модуль операционно ощущаться как встроенная интеграция Kora.

Следуйте знакомым конвенциям конфигурации телеметрии

Если встроенные модули Kora используют такие концепции, как:

telemetry {
    logging {
        enabled = false
    }

    metrics {
        enabled = true
    }

    tracing {
        enabled = true
    }
}
telemetry:
    logging:
        enabled: false

    metrics:
        enabled: true

    tracing:
        enabled: true

кастомный модуль должен серьёзно рассмотреть похожую форму.

Например:

search {
    endpoint = "https://search.internal"

    telemetry {
        logging {
            enabled = false
        }

        metrics {
            enabled = true
        }

        tracing {
            enabled = true
        }
    }
}
search:
    endpoint: "https://search.internal"

    telemetry:
        logging:
            enabled: false

        metrics:
            enabled: true

        tracing:
            enabled: true

Разработчики, уже знакомые с Kora, немедленно это поймут.

Первоклассный означает связный с фреймворком, а не просто совместимый с ним.

Решите, раскрывать ли нативный клиент или тонкий адаптер

Есть два хороших дизайна.

Нативный клиент как компонент

бизнес-сервис
  ↓
SearchClient

Это идеально, когда сторонний клиент уже раскрывает полезные хуки для транспорта, телеметрии или жизненного цикла.

Тонкий инструментированный адаптер

бизнес-сервис
  ↓
InstrumentedSearchClient
  ↓
SearchClient

Это полезно, когда телеметрию или политику нельзя чисто прикрепить к нативному клиенту.

Обёртка должна заслуживать своё существование.

Если она только переименовывает:

delegate.search(request);
delegate.search(request)

она даёт мало ценности.

Полезная обёртка должна вносить что-то реальное:

телеметрию
маппинг
политику
владение ресурсами
маршрутизацию
стабильный внутренний API

Конкретная архитектура модуля поиска

Разумная архитектура может быть:

SearchConfig
    ↓
CredentialsProvider
    ↓
SearchTransport
    ↓
SearchTelemetry
    ↓
SearchClient
    ↓
CatalogSearchService

Типизированная конфигурация:

@ConfigMapper
public interface SearchConfig {

    URI endpoint();

    String apiKey();

    default Duration connectTimeout() {
        return Duration.ofSeconds(2);
    }

    default Duration requestTimeout() {
        return Duration.ofSeconds(5);
    }
}
@ConfigMapper
interface SearchConfig {

    fun endpoint(): URI

    fun apiKey(): String

    fun connectTimeout(): Duration {
        return Duration.ofSeconds(2)
    }

    fun requestTimeout(): Duration {
        return Duration.ofSeconds(5)
    }
}

Простой адаптер телеметрии:

public final class InstrumentedSearchClient {

    private final SearchClient delegate;
    private final SearchTelemetry telemetry;

    public InstrumentedSearchClient(
        SearchClient delegate,
        SearchTelemetry telemetry
    ) {
        this.delegate = delegate;
        this.telemetry = telemetry;
    }

    public SearchResult search(SearchRequest request) {
        var ctx = telemetry.create(
            new SearchOperation("search", request.index())
        );

        try {
            var result = delegate.search(request);
            ctx.close(null);
            return result;
        } catch (Throwable e) {
            ctx.close(e);
            throw e;
        }
    }
}
class InstrumentedSearchClient(
    private val delegate: SearchClient,
    private val telemetry: SearchTelemetry
) {

    fun search(request: SearchRequest): SearchResult {
        val ctx = telemetry.create(
            SearchOperation("search", request.index())
        )

        try {
            val result = delegate.search(request)
            ctx.close(null)
            return result
        } catch (e: Throwable) {
            ctx.close(e)
            throw e
        }
    }
}

Граф приложения обрабатывает конструирование.

Бизнес-код видит обычный типизированный клиент.

Держите фабричные методы маленькими

Избегайте гигантской фабрики, такой как:

default SearchEverything everything(
    Config config,
    MeterRegistry registry,
    OpenTelemetry telemetry,
    CredentialsProvider credentials,
    TlsFactory tls,
    Executor executor,
    ...
) {
    // сотни строк
}
fun everything(
    config: Config,
    registry: MeterRegistry,
    telemetry: OpenTelemetry,
    credentials: CredentialsProvider,
    tls: TlsFactory,
    executor: Executor,
    ...
): SearchEverything {
    // сотни строк
}

Предпочитайте меньшие узлы графа:

SearchConfig
  ↓
SearchCredentials
  ↓
SearchTransport
  ↓
SearchTelemetry
  ↓
SearchClient

Внедрение зависимостей на этапе компиляции хорошо собирает компоненты.

Позвольте Kora делать эту работу, а не создавать частный мини-контейнер внутри одного метода.

Проектируйте явные точки переопределения

Общий модуль может предоставлять:

DefaultSearchTelemetry
DefaultCredentialsProvider
DefaultTransportFactory

но некоторым приложениям понадобятся замены.

Хорошие точки расширения часто включают:

CredentialsProvider
TransportFactory
TelemetryFactory
ClientCustomizer

Не раскрывайте каждый внутренний объект как публичный API.

Раскрывайте стабильные границы там, где кастомизация действительно полезна.

Это предотвращает форки без замораживания деталей реализации.

Заменяемость — часть «создан для расширения»

Расширяемость — это не только способность добавлять новые компоненты.

Это также способность заменять значения по умолчанию.

Общий модуль поиска может по умолчанию использовать статические учётные данные API-ключа, в то время как одна производственная платформа использует workload identity.

Модуль должен допускать эту замену через DI.

То же самое применимо к телеметрии, TLS, транспорту и выбранному поведению конструирования клиента.

Первоклассный модуль переиспользуем, потому что предоставляет сильные значения по умолчанию, не будучи жёстким.

Несколько экземпляров требуют намеренного дизайна

Приложения часто перерастают допущение об одном глобальном клиенте.

Примеры:

основной поисковый кластер
аналитический поисковый кластер

NATS плоскости управления
NATS плоскости событий

внутреннее объектное хранилище
внешнее объектное хранилище

Паттерны тегов и фабричных модулей Kora могут представлять несколько независимо настроенных экземпляров.

Автор модуля должен сознательно выбирать, поддерживается ли многоэкземплярное использование.

Не делайте его случайно невозможным, пряча всё состояние в одном статическом синглтоне.

Фабричные модули помогают параметризованным интеграциям

Иногда одна переиспользуемая реализация модуля должна параметризоваться путём конфигурации или логической идентичностью.

Концептуально:

SearchFactoryModule("search.primary")
SearchFactoryModule("search.analytics")

может создавать два независимых набора:

SearchConfig
SearchTelemetry
SearchClient

с тегами, различающими их.

Это мощная функция, потому что кастомная инфраструктура может использовать те же примитивы графа, которые Kora использует для собственных параметризованных модулей.

Сторонние интеграции не ограничены более слабой моделью расширения.

Теги должны выражать смысл

Теги уместны, когда два компонента имеют один тип, но разные семантические роли:

@PrimarySearch SearchClient
@AnalyticsSearch SearchClient

Это передаёт архитектуру.

Теги менее полезны, когда добавляются произвольно для исправления плохо спроектированной неоднозначности графа.

Тег должен говорить читателю, почему компоненты различаются.

Elasticsearch: что нужно первоклассному модулю

Хороший модуль Elasticsearch должен, вероятно, интегрировать:

типизированную конфигурацию
учётные данные
TLS
транспорт HTTP
нативный клиент
жизненный цикл клиента/транспорта
телеметрию
опциональную информацию о здоровье
поддержку тестирования

Концептуальный граф:

ElasticsearchConfig
        ↓
CredentialsProvider
        ↓
Transport
        ↓
ElasticsearchClient
        ↓
SearchService

Нативный API Elasticsearch должен оставаться видимым, если организация намеренно не хочет стабильную внутреннюю абстракцию.

Телеметрию может быть лучше всего прикреплять на уровне транспорта, если нативный клиент предоставляет подходящие хуки. Обёртывание каждого высокоуровневого вызова API вручную может быть хрупким.

Цель — использовать нативные точки расширения, где возможно, и добавлять интеграцию Kora вокруг них.

Не двойной инструментируйте Elasticsearch

Современные клиенты и транспорты могут уже раскрывать инструментацию, совместимую с OpenTelemetry.

Если нативная библиотека уже создаёт полезный span клиента, а модуль Kora создаёт идентичный span вокруг него, трассировки становятся шумными:

search
  ↓
search
    ↓
HTTP-запрос

Поймите модель наблюдаемости библиотеки, прежде чем добавлять ещё один слой.

Первоклассный модуль компонуется с нативной инструментацией, а не слепо дублирует её.

NATS: жизненный цикл — главная задача

Интеграция NATS может включать:

соединение
состояние переподключения
контекст JetStream
подписки
диспетчеры
обработчики сообщений
слив
сброс
завершение

Возможный граф:

NatsConfig
   ↓
NatsConnection
   ├── NatsPublisher
   ├── JetStream
   └── NatsConsumerManager
          ↓
       обработчики

Здесь жизненный цикл гораздо важнее простого конструирования.

Менеджеру потребителей может понадобиться запускать подписки после готовности соединения и сливать их до закрытия соединения.

Телеметрия должна покрывать операции публикации и обработку сообщений.

Хорошая интеграция должна операционно раскрывать поведение переподключения и потребителя.

Компоненты с побочными эффектами могут нуждаться в статусе корневых

Менеджер потребителей может существовать только потому, что его жизненный цикл запускает подписки.

Никакой другой компонент не обязательно его внедряет.

В графе зависимостей, управляемом спросом, такой компонент может исчезнуть, если ничто не тянет его в граф.

Инфраструктура, само существование которой является поведением приложения, может нуждаться в пометке как корневой компонент.

Примеры включают:

менеджер потребителей сообщений
фоновый наблюдатель
планировщик
запускатель подписок

Не помечайте всё как корневое.

Используйте статус корневого для компонентов, чьи побочные эффекты намеренно являются частью работающего приложения.

MinIO: сохраняйте семантику объектного хранилища видимой

Модуль MinIO обычно ближе к клиентскому модулю.

Конфигурация может включать:

endpoint
access key
secret key
region
таймауты
TLS

Телеметрия может описывать:

get_object
put_object
remove_object
stat_object
bucket
длительность
размер полезной нагрузки
результат

Избегайте таких измерений метрик, как сырой ключ объекта.

Не создавайте автоматически bucket'ы при каждом запуске приложения, если это не явная функция плоскости управления.

Сервис, использующий MinIO для операций плоскости данных, не должен автоматически требовать привилегий администрирования bucket'ов.

Отделяйте плоскость управления от плоскости данных

Этот принцип применим во всех технологиях.

Примеры действий плоскости управления:

создать индекс Elasticsearch
установить шаблоны
создать поток NATS
изменить срок хранения
создать bucket объектного хранилища

Примеры действий плоскости данных:

поиск
публикация
потребление
положить объект
взять объект

Реплики приложения обычно нуждаются в плоскости данных.

Им могут не понадобиться административные привилегии.

Первоклассный модуль должен избегать смешивания работы развёртывания/настройки в каждый запуск сервиса, если архитектура явно этого не требует.

Внутренняя библиотека X использует тот же паттерн

Предположим, компания владеет:

FraudModelClient

Ему нужны типизированная конфигурация, TLS, учётные данные, телеметрия, обновление модели и корректное завершение.

Модуль Kora может раскрывать:

FraudModelConfig
FraudModelTelemetry
FraudModelClient
FraudModelModelWatcher
FraudModelProbe

Паттерн идентичен.

Именно здесь модульность фреймворка становится организационно ценной: специфичная для компании инфраструктура может участвовать в той же модели приложения, что и встроенные интеграции.

Держите публичную поверхность модуля маленькой

Как только несколько сервисов зависят от модуля, каждый публичный тип становится обещанием совместимости.

Раскрывайте только то, что действительно нужно потребителям и авторам расширений.

Полезная форма может быть:

SearchModule
SearchConfig
SearchClient
SearchTelemetry
SearchTelemetryFactory

Детали реализации должны оставаться приватными.

Это облегчает обновления и держит модуль понятным.

Отделяйте основную библиотеку от интеграции Kora

Если вы владеете библиотекой X, рассмотрите:

library-core
library-kora

вместо того чтобы заставлять саму основную библиотеку зависеть от Kora.

Тогда основную библиотеку можно использовать:

внутри Kora
вне Kora
в утилитах командной строки
в автономных тестах

а модуль Kora действует как адаптер фреймворка.

Это сохраняет ответственности чистыми.

Тонкозернистые зависимости важны

Кастомный модуль должен зависеть только от тех возможностей Kora, которые реально использует.

Если интеграции MinIO нужны конфигурация и телеметрия, она не должна тянуть HTTP-сервер, базу данных, Kafka и несвязанные модули.

Аналогично, опциональные утилиты тестирования не должны быть производственными зависимостями.

Это следует более широкому принципу Kora:

включайте только то, что нужно сервису

Раздутая интеграция может подрывать модульный рантайм фреймворка.

Опциональные функции должны быть действительно опциональными

Предположим, трассировка отключена.

Модуль должен уметь использовать дешёвый no-op путь телеметрии вместо требования ненужного поведения экспортёра.

Предположим, пробы не используются.

Клиент не должен запускать фоновый проверщик здоровья только потому, что интеграция его поддерживает.

Опциональные функции не должны молча тянуть большие подграфы рантайма в каждое приложение.

Пробы — это решение политики

Должен ли недоступный кластер Elasticsearch делать приложение неготовым?

Иногда да.

Иногда нет.

Сервис, который не может выполнять никакую полезную работу без поиска, может обоснованно проваливать готовность.

Сервис, который может откатиться на PostgreSQL, вероятно, не должен убирать себя из трафика только потому, что поиск лежит.

Поэтому переиспользуемый модуль обычно должен раскрывать информацию о здоровье, не навязывая универсальную семантику готовности или живости.

Приложение решает, как здоровье зависимостей отображается на допуск трафика.

Не превращайте сбой удалённой зависимости в сбой живости

Перезапуск приложения редко чинит недоступный внешний сервис.

Отключение NATS, отключение Elasticsearch или отключение MinIO не должно автоматически заставлять каждую реплику сервиса проваливать живость и перезапускаться.

Это может превратить один инфраструктурный инцидент в шторм перезапусков всего флота.

Дизайн проб принадлежит архитектуре модуля, а не только конфигурации фреймворка.

Проверки здоровья должны быть дешёвыми

Проба не должна выполнять дорогой поиск или загрузку объекта каждую секунду из каждого Pod.

Это создаёт производственную нагрузку от самого мониторинга.

Предпочитайте:

нативный лёгкий эндпоинт здоровья
состояние соединения
ограниченный ping
кешированное состояние здоровья

в зависимости от технологии.

Наблюдаемость не должна становиться атакой на зависимость.

Обновление конфигурации требует намеренной политики

Kora может обновлять затронутые компоненты графа при изменении конфигурации.

Автор модуля должен спросить:

Можно ли безопасно пересоздать этот клиент?
Должны ли потребители тоже пересоздаваться?
Должен ли какой-то долгоживущий компонент наблюдать новый клиент косвенно?
Требует ли это изменение перезапуска процесса?

Не наследуйте поведение обновления случайно.

Внешние клиенты могут владеть сокетами, подписками, потоками или дорогим состоянием.

Перезагрузка конфигурации мощна и должна отражать семантику ресурсов.

Прямые зависимости и ValueOf означают разную связанность жизненного цикла

Прямая зависимость графа означает, что потребитель связан по жизненному циклу с зависимостью.

Косвенная зависимость ValueOf<T> может позволить долгоживущему компоненту наблюдать текущее значение без пересборки при каждом изменении этой зависимости.

Это полезно для инфраструктуры, которая должна оставаться работающей, пока одна из её зависимостей обновляется.

Но не используйте косвенность просто для избежания корректного отношения зависимости.

Используйте её, когда живая замена — реальное архитектурное требование.

Тестирование — часть дизайна модуля

Первоклассный модуль должен быть тестируем на нескольких уровнях.

Модульные тесты модуля

Валидируйте:

маппинг конфигурации
валидацию
классификацию телеметрии
логику фабрики
маппинг ошибок

Интеграционные тесты модуля

Используйте реальную инфраструктуру там, где важна семантика протокола:

подключение
аутентификацию
выполнение операции
наблюдение телеметрии
завершение

Тесты приложения-потребителя

Позвольте приложению заменять реальный клиент фейковой или тестовой реализацией.

Каждый уровень отвечает на разные вопросы.

Замена компонентов — основной тестовый шов

Тест сервиса должен уметь заменять:

SearchClient

на:

FakeSearchClient

без запуска Elasticsearch.

Затем отдельный интеграционный набор упражняет реальный модуль клиента.

Это здоровое разделение:

бизнес-тесты
→ фейковая инфраструктура

интеграционные тесты
→ реальная инфраструктура

DI должен делать это лёгким.

Если модуль затрудняет замену, его интеграционная граница, вероятно, слишком жёсткая.

Реальные интеграционные тесты всё равно необходимы

Моки не могут доказать:

поведение TLS
аутентификацию
совместимость протокола
семантику запросов Elasticsearch
подтверждение NATS
многокомпонентное поведение MinIO
нативный повтор
сетевой таймаут
завершение

Самому модулю нужно реальное интеграционное покрытие.

Testcontainers или эквивалентная одноразовая инфраструктура идеальны, где доступны.

Быстрый запуск графа Kora помогает делать такие тесты практичными.

Тестируйте жизненный цикл, а не только бизнес-операции

Тест модуля должен проверять больше, чем:

client.search() возвращает результат

Также тестируйте:

сбой запуска
очистку при частичном запуске
завершение
повторные запуск/остановку, где релевантно
обработку операций в полёте
закрытие ресурсов

Многие интеграционные баги появляются только во время раскатывающегося развёртывания или частичного сбоя.

Первоклассным модулям нужны тесты жизненного цикла.

Тестируйте семантику телеметрии

Проверяйте:

метрика успеха испускается один раз
классификация сбоя стабильна
span закрывается при исключении
секреты не логируются
имена операций остаются ограниченными

Дашборды и алерты могут зависеть от этих контрактов.

Телеметрия — часть API модуля.

Инъекция сбоев важна

Инфраструктурные модули должны тестировать:

отказ соединения
сбой аутентификации
таймаут
ошибку удалённого сервера
отключение брокера
недопустимую конфигурацию
завершение во время работы

и проверять:

тип ошибки
очистку
телеметрию
поведение проб

Производственное качество модуля во многом определяется его путём сбоя.

Ошибки запуска должны быть действенными

Если инициализация падает, ошибка должна говорить разработчику:

какая интеграция
какой эндпоинт
какая фаза жизненного цикла
какая базовая ошибка произошла

Избегайте расплывчатой обёртки, такой как:

RuntimeException: init failed

Kora может показывать сбой инициализации графа, но модуль должен сохранять достаточно деталей, чтобы ошибка графа была полезной.

Частичная инициализация требует очистки

Рассмотрим:

соединение создано
подписка A зарегистрирована
подписка B падает

Интеграция должна освободить A и соединение.

Код жизненного цикла, предполагающий, что запуск всегда завершается, может утекать ресурсы.

Простые клиенты могут использовать простые обёртки.

Сложные системы подписок могут заслуживать выделенного менеджера с явным состоянием.

Отделяйте менеджеров, когда ответственности различаются

Архитектура NATS может выигрывать от:

NatsConnection
NatsConsumerManager

Соединение владеет транспортом.

Менеджер владеет подписками и жизненным циклом обработки сообщений.

Во время завершения:

менеджер потребителей сливает
  ↓
соединение закрывается

Граф естественно представляет этот порядок.

Не перегружайте один компонент несвязанными ответственностями только для минимизации количества классов.

Владение ресурсами должно быть явным

Для каждого ресурса ответьте:

Кто его создаёт?
Кто его закрывает?
Можно ли его разделять?
Может ли приложение предоставить свой?

Например:

транспорт HTTP поиска
  создаётся модулем
  принадлежит модулю
  закрывается модулем

трейсер OpenTelemetry
  создаётся приложением/платформой
  внедряется в модуль
  не закрывается модулем

Компонент никогда не должен закрывать общую зависимость, которой не владеет.

Владение — часть дизайна жизненного цикла.

Избегайте скрытых пулов потоков

Сторонние клиенты могут создавать собственные воркеры или event-loop'ы.

Если модуль поддерживает несколько экземпляров клиента, количество потоков может неожиданно умножиться.

Документируйте важное поведение ресурсов.

Где безопасно и поддерживается, позволяйте внедрять общие исполнители или транспорты.

Не прячьте дорогие фоновые ресурсы за маленько выглядящей фабрикой клиента.

Потоки должны уважать нативную библиотеку

Некоторые клиенты синхронны.

Некоторые асинхронны.

Некоторые управляют собственными event-loop'ами.

Модель виртуальных потоков Kora 2 означает, что синхронный блокирующий клиент часто может оставаться синхронным без реактивной обёртки.

Не добавляйте автоматически исполнители или асинхронные адаптеры ради эстетической согласованности.

Тонкая интеграция означает уважение нативной модели исполнения.

Отказоустойчивость должна быть компонуемой, а не скрыто захардкоженной

Должен ли каждый поиск повторяться три раза?

Должна ли каждая загрузка объекта использовать circuit breaker?

Должна ли каждая публикация NATS автоматически повторяться?

Универсального ответа нет.

Предпочитайте:

нативный повтор транспорта, где уместно
+
политику отказоустойчивости приложения/Kora, где семантически уместно

вместо скрытия агрессивного поведения внутри конструирования клиента.

Модуль должен документировать нативную семантику повтора, чтобы повтор уровня приложения случайно не умножал попытки.

Усиление повторов легко создать

Предположим:

нативный повтор клиента = 3
@Retry Kora = 3

Система может выполнить до девяти попыток.

При сбое зависимости это может резко усилить нагрузку.

Телеметрия модуля должна делать попытки и сбои видимыми, где возможно.

Документация должна объяснять нативное поведение.

Интеграция фреймворка должна уменьшать сюрпризы, а не создавать их.

Слои таймаутов требуют той же осторожности

Нативный таймаут запроса и внешний таймаут Kora не обязательно одно и то же.

Например:

внешний таймаут операции = 2s
нативный таймаут клиента = 10s

Если отмена слабая, приложение может перестать ждать, в то время как базовая операция продолжает потреблять ресурсы.

Первоклассный модуль должен документировать поведение отмены и таймаутов базовой библиотеки.

Операционная корректность зависит от этого.

Конфигурация безопасности должна быть явной

Учётные данные не должны извлекаться из случайных переменных среды внутри глубокого кода реализации.

Предпочитайте ясную зависимость:

CredentialsProvider

или типизированную конфигурацию секретов.

Это позволяет приложениям выбирать:

статический секрет
Vault
облачную workload identity
ротацию учётных данных

без изменения бизнес-кода.

Отделение получения учётных данных от конструирования клиента — отличный шов расширения.

TLS — часть интеграционного контракта

Серьёзным модулям нужно учитывать:

кастомный CA
клиентский сертификат
проверку имени хоста
политику протокола TLS

Не делайте небезопасный TLS лёгкой конфигурацией.

Если у организации есть общие TLS-компоненты, интегрируйтесь с ними.

Иначе раскрывайте ясные типизированные опции.

Безопасность — не запоздалая мысль к конструированию клиента.

Никогда не утекайте секреты через телеметрию

Модуль видит учётные данные и потенциально чувствительные данные запросов.

Он должен предотвращать случайное логирование:

API-ключа
заголовка Authorization
пароля
секретного access key
подписанного URL
токена учётных данных

Маскирование принадлежит общей интеграции, чтобы каждый сервис не изобретал его заново.

Централизованный код модуля — множитель силы и для хорошей, и для плохой безопасности.

Начните с DI, прежде чем строить генерацию кода

Kora широко использует процессоры аннотаций, но кастомному модулю автоматически не нужен собственный.

Разумная последовательность:

Фаза 1:
фабрики модуля

Фаза 2:
типизированная конфигурация + жизненный цикл

Фаза 3:
телеметрия + тесты

Фаза 4:
поддержка нескольких экземпляров

Фаза 5:
генерация, только если повторяющиеся статические контракты её оправдывают

Не стройте процессор только потому, что у самой Kora есть процессоры.

Используйте генерацию кода, когда есть реальная структура этапа компиляции, которую стоит генерировать.

Первоклассный модуль может иметь почти нулевой API Kora в точке использования

Это часто идеальный результат.

Сам модуль использует:

модули
маппинг конфигурации
жизненный цикл
телеметрию
теги

но бизнес-код просто внедряет:

SearchClient
SearchClient

и вызывает его.

Это значит, что интеграция фреймворка удалась без загрязнения обычного использования технологии.

Kora богата на границе сборки и тонка в модели программирования рантайма.

Сравните тонкую интеграцию со стеком обёрток

Переусложнённая интеграция может стать:

SearchService
    ↓
SearchTemplate
    ↓
SearchOperations
    ↓
SearchAdapter
    ↓
SearchGateway
    ↓
SearchClient

Каждый слой добавляет маппинг, исключения, конфигурацию и телеметрию.

Это выглядит «полным», но увеличивает поверхность обучения, время отладки и стоимость обновления.

Тонкая интеграция:

SearchService
    ↓
SearchClient

оставляет технологию узнаваемой.

Тонкие модули обновляются легче

Когда меняется нативный клиент, тонкий модуль в основном обновляет:

конструирование
хуки транспорта
жизненный цикл
интеграцию телеметрии

Большой специфичной для фреймворка абстракции может понадобиться переводить множество функций вендора между версиями.

Низкая семантическая дистанция снижает стоимость обслуживания.

Это особенно важно для инфраструктурных библиотек, которые быстро развиваются.

Политика версионирования важна

Общий модуль должен документировать:

поддерживаемую версию нативного клиента
совместимость с Kora
допущения управления зависимостями
раскрывается ли зависимость нативного клиента

Не существует одной правильной схемы версионирования.

Но политика должна быть намеренной.

Слишком большая гибкость может производить несовместимые комбинации зависимостей.

Слишком жёсткая фиксация может блокировать обновления безопасности.

Конфликты зависимостей — это инженерия модуля

Инфраструктурные клиенты часто зависят от:

Netty
Jackson
OkHttp
protobuf
API логирования

Переиспользуемый модуль должен сосуществовать с более широким стеком Kora.

Избегайте ненужного shading или приватных копий фундаментальных библиотек.

Выравнивайте через управление зависимостями, где это практично.

Первоклассная интеграция — не только API-клей; это также гигиена зависимостей.

Схема телеметрии модуля — это API

Изменение метрики с:

search.request.duration

на:

search.operation.duration

может сломать дашборды.

Изменение меток может сломать запросы алертов.

Изменение именования спанов может сломать поиск по трассировкам.

Совместимость телеметрии важна, когда модуль широко развёрнут.

Версионируйте её намеренно.

Модуль становится примитивом организационной платформы

Первый сервис может интегрировать библиотеку X локально.

Пятый сервис, вероятно, не должен снова копировать ту же настройку.

Общий модуль Kora может централизовать:

конструирование
таймауты
TLS
учётные данные
телеметрию
завершение
версии зависимостей
поддержку тестирования

Каждый сервис получает накопленные инфраструктурные знания.

Именно здесь модульность становится механизмом организационного масштабирования.

Не извлекайте общие модули слишком рано

Один сервис с одной фабрикой не обязательно оправдывает артефакт платформы.

Полезная эволюция:

локальная интеграция
  ↓
второй реальный потребитель
  ↓
определить стабильное общее поведение
  ↓
извлечь переиспользуемый модуль

Преждевременные инфраструктурные абстракции часто замораживают не тот API.

Позвольте реальному использованию раскрыть стабильную интеграционную границу.

Первоклассный не значит официальный

Модуль компании может быть таким же первоклассным, как интеграция, поддерживаемая Kora, если он корректно участвует в:

графе
конфигурации
жизненном цикле
телеметрии
тестировании
эксплуатации

Это практический тест расширяемости.

Пользователям не должны требоваться модификации ядра Kora, чтобы строить продакшен-классные интеграции.

Компилятор — часть поверхности расширения

Объявляя фабрики модулей и типизированные зависимости, кастомные интеграции автоматически выигрывают от компилятора Kora:

обнаружение отсутствующих зависимостей
обнаружение неоднозначности
обнаружение циклов
сгенерированная связка

Автору модуля не нужен движок начальной загрузки рантайма.

Это мощное свойство.

Компилятор фреймворка становится инфраструктурой, общей для экосистемы.

Сгенерированная связка помогает отлаживать кастомные модули

Если интеграция ведёт себя странно, разработчики могут изучить сгенерированную связку приложения и увидеть:

какая фабрика создала клиент
какие зависимости были внедрены
какая реализация победила

Это особенно полезно для модулей уровня организации, поддерживаемых платформенной командой.

Потребители не вынуждены доверять невидимой авто-конфигурации.

Они могут изучить, что построила Kora.

Нейро-агенты выигрывают от явных модулей

нейро-агент может следовать:

@KoraApp
public interface Application extends SearchClientModule {
}
@KoraApp
interface Application : SearchClientModule

затем изучить:

SearchConfig
фабрики модуля
LifecycleWrapper
SearchTelemetry
сгенерированный граф

Если граф неполон, компиляция даёт точную обратную связь.

Магии classpath, которую нужно выводить, мало.

Это ровно тот вид архитектуры, который хорошо работает с разработкой инфраструктуры при поддержке агентов.

Сильные типы улучшают и DI, и нейро-технологии

Избегайте API модулей, в которых доминируют:

Object
Map<String, Object>
универсальные мешки контекста

когда существует более сильный тип.

Предпочитайте:

SearchConfig
SearchTelemetryFactory
SearchClient
CredentialsProvider

Сильные контракты улучшают валидацию графа, навигацию в IDE, документацию и рассуждение с помощью нейро-технологий.

Качество типов напрямую влияет на качество инфраструктуры.

Избегайте API сервис-локатора

Kora поддерживает продвинутый доступ к графу для случаев, которые действительно в нём нуждаются.

Не используйте его для обычного конструирования.

Предпочитайте:

default SearchClient searchClient(
    SearchConfig config,
    SearchTelemetry telemetry
) {
    ...
}
fun searchClient(
    config: SearchConfig,
    telemetry: SearchTelemetry
): SearchClient {
    ...
}

вместо:

default SearchClient searchClient(Graph graph) {
    // динамические поиски
}
fun searchClient(graph: Graph): SearchClient {
    // динамические поиски
}

Типизированные параметры зависимостей сохраняют валидацию этапа компиляции.

Используйте поиск по графу только когда динамическое поведение графа действительно часть проблемы.

Избегайте построения второго контейнера внутри фабрики

Если один метод модуля содержит сложные условные поиски, ручное кеширование синглтонов и динамическое конструирование, модуль воссоздаёт DI-контейнер внутри Kora.

Вместо этого выражайте архитектуру как компоненты графа, где практично.

Позвольте Kora разрешать зависимости.

Фабричные методы должны создавать значения, а не реализовывать скрытый фреймворк приложения.

Опциональные функции не должны тянуть неиспользуемую инфраструктуру

Если модуль поддерживает:

трассировку
admin API
проверки здоровья
управление JetStream
подготовку индексов

но приложению нужен только базовый клиент, оно не должно инициализировать всё.

Это рекурсивно применяет собственный принцип модульности Kora:

используйте только то, что нужно

Кастомный модуль не должен становиться монолитом внутри модульного фреймворка.

Разделяйте большие интеграции, когда границы становятся реальными

Зрелая интеграционная экосистема может в итоге иметь:

kora-nats-core
kora-nats-jetstream
kora-nats-testing

или:

kora-search-client
kora-search-admin
kora-search-testing

Делайте это только когда границы снижают зависимости или поведение рантайма.

Больше Gradle-модулей не автоматически означает лучшую модульность.

Разделение должно соответствовать значимым границам возможностей.

Утилиты тестирования могут жить отдельно

Производственный артефакт не должен зависеть от Testcontainers или большой фейковой инфраструктуры.

Поддержка тестирования может жить в выделенном артефакте:

SearchTestKit
NatsTestContainer
MinioFixture
FakeSearchClient

Это сохраняет чистый производственный граф зависимостей, стандартизируя настройку тестов для потребителей.

Идеальный опыт разработчика

Потребитель готового модуля должен уметь сделать что-то близкое к:

@KoraApp
public interface Application extends SearchClientModule {
}
@KoraApp
interface Application : SearchClientModule

предоставить:

search {
    endpoint = "https://search.internal"
    apiKey = ${SEARCH_API_KEY}
}
search:
    endpoint: "https://search.internal"
    apiKey: "${SEARCH_API_KEY}"

и внедрить:

@Component
public final class ProductSearchService {

    private final SearchClient search;

    public ProductSearchService(SearchClient search) {
        this.search = search;
    }
}
@Component
class ProductSearchService(private val search: SearchClient)

Разработчик приложения не должен вручную:

разбирать конфигурацию
строить транспорт
регистрировать хуки завершения
конструировать метрики
создавать спаны
маскировать учётные данные
закрывать ресурсы

Модуль владеет этими механизмами.

В то же время, если что-то идёт не так, фабрики и сгенерированный граф остаются инспектируемыми.

Эта комбинация — идеал Kora:

лёгкое значение по умолчанию
+
явный механизм

Идеальный опыт оператора

Оператор должен видеть кастомную интеграцию через те же инструменты, что и встроенные модули Kora:

операционные метрики
распределённые спаны
структурированные ошибки
логи жизненного цикла
состояние проб, где настроено

Не должно быть отдельной экосистемы наблюдаемости просто потому, что клиент пришёл из сторонней библиотеки.

Первоклассный означает и операционно нативный.

Идеальный опыт платформенной команды

Платформенная команда должна уметь публиковать:

company-kora-elasticsearch
company-kora-nats
company-kora-minio

и знать, что каждый сервис получает стандартизированные:

конфигурацию
таймауты
TLS
учётные данные
телеметрию
завершение
поддержку тестирования

Именно здесь утверждение «создан для расширения» становится стратегически важным.

Kora не нуждается в каждой возможной интеграции в собственном репозитории, если команды могут создавать интеграции, достигающие того же уровня качества, используя публичные примитивы фреймворка.

Модульность позволяет сфокусированному фреймворку оставаться сфокусированным

Kora намеренно избегает попыток обернуть каждую технологию за одной огромной абстракцией фреймворка.

Эта философия работает, только если расширение практично.

Иначе каждая отсутствующая интеграция становится ограничением фреймворка.

Здоровая модель:

ядро Kora
предоставляет модель приложения

модуль технологии
адаптирует библиотеку в эту модель

приложение
выбирает модуль явно

Фреймворк может оставаться сфокусированным, пока экосистема растёт.

Качество расширения важнее количества расширений

Фреймворк с сотнями интеграций не обязательно более расширяем.

Важный вопрос:

Может ли компетентная команда построить следующую интеграцию, не сражаясь со скрытыми внутренностями фреймворка?

Kora предоставляет сырьё:

фабрики модулей
DI этапа компиляции
типизированную конфигурацию
жизненный цикл
обёртки
теги
корневые компоненты
телеметрию
тестирование
замену компонентов

Если этого достаточно для построения продакшен-классной интеграции, модель расширения здорова.

Хороший модуль должен стать скучным

Как только модуль готов, разработчики приложения должны редко думать о его инфраструктурных механизмах.

Они должны внедрять клиент и писать бизнес-код.

Операторы должны получать предсказуемую телеметрию.

Тесты должны чисто заменять компонент.

Завершение должно корректно освобождать ресурсы.

Конфигурация должна вести себя предсказуемо.

Скучная инфраструктура — это успех.

«Создан для расширения» означает, что модель приложения остаётся открытой

Самый глубокий момент в том, что собственная модель приложения Kora доступна авторам расширений.

Вы можете использовать те же базовые идеи:

модули
фабричные методы
типизированную конфигурацию
жизненный цикл
зависимости графа
теги
корневые компоненты
телеметрию
тестовые замены

чтобы построить собственную производственную интеграцию.

Вы не ограничены слабым API плагинов, в то время как модули, принадлежащие фреймворку, используют привилегированные внутренние механизмы.

Именно это делает интеграцию способной стать первоклассной.

Практическая последовательность разработки

При интеграции новой библиотеки хорошая последовательность:

1. Интегрируйте её локально в одном реальном сервисе.
2. Поймите нативное конструирование и владение.
3. Извлеките типизированную конфигурацию.
4. Переместите конструирование в фабрики DI.
5. Добавьте жизненный цикл.
6. Добавьте телеметрию.
7. Добавьте реальные интеграционные тесты.
8. Добавьте швы замены для потребителей.
9. Добавьте поддержку нескольких экземпляров только если нужно.
10. Добавьте генерацию кода только если статическое повторение её оправдывает.

Это держит модуль привязанным к реальным требованиям.

Не начинайте с проектирования универсальной абстракции, которая, как вы надеетесь, понадобится когда-нибудь.

Измеряйте модуль под реальной нагрузкой

Кастомная интеграция может влиять на:

время запуска
готовность
память
количество потоков
количество соединений
задержку запросов
длительность завершения

Бенчмаркайте её.

Наблюдайте поведение переподключения.

Тестируйте отключения зависимостей.

Измеряйте накладные расходы телеметрии.

Проверяйте завершение под нагрузкой.

Модуль становится первоклассным через производственное поведение, а не через наличие @Module.

Плохой модуль может стереть преимущества фреймворка

Если Kora достигает готовности быстро, но ваш кастомный клиент тратит пятнадцать секунд на последовательное обнаружение кластера, прогрев кешей и открытие сотен соединений, приложение всё равно медленное.

Кастомная интеграция теперь часть пути фреймворка с точки зрения приложения.

Быстрые внутренности фреймворка не компенсируют медленный код расширения.

Авторам модулей нужна та же дисциплина производительности, что и авторам фреймворка.

Жизненный цикл и телеметрия отделяют «работает» от «эксплуатируется»

Фабричный метод заставляет библиотеку работать.

Типизированная конфигурация делает её разворачиваемой.

Жизненный цикл делает её безопасной во владении.

Телеметрия делает её эксплуатируемой.

Тестирование делает её поддерживаемой.

Внедрение зависимостей на этапе компиляции делает её структурно проверяемой.

Вместе эти свойства делают интеграцию первоклассной.

Вот почему полезный вопрос не:

Как мне поместить этот клиент в DI Kora?

Лучший вопрос:

Что нужно этой технологии, чтобы вести себя как нативный производственный компонент приложения на Kora?

Основной паттерн

Для большинства интеграций архитектура сходится к:

нативная библиотека
     ↓
типизированная конфигурация
     ↓
фабрика / транспорт
     ↓
клиент, управляемый жизненным циклом
     ↓
адаптер телеметрии
     ↓
граф приложения Kora
     ↓
бизнес-код

Опциональные слои включают:

пробу
отказоустойчивость
фабрику нескольких экземпляров
тест-кит
сгенерированные адаптеры

Точная реализация меняется с технологией.

Принципы остаются стабильными.

Заключение

Философия Kora «создан для расширения» ценна не потому, что фреймворк допускает ещё одну аннотацию или ещё один фабричный метод. Она ценна потому, что собственная модель приложения Kora достаточно открыта, чтобы внешние интеграции становились настоящими равными встроенным модулям.

Серьёзный модуль Kora делает больше, чем инстанцирует клиент.

Он даёт технологии место в графе зависимостей этапа компиляции.

Он маппит конфигурацию развёртывания в сильные типы.

Он делает владение ресурсами и завершение явными.

Он прикрепляет логирование, метрики и трассировку через тот же операционный стек, который использует остальное приложение.

Он предоставляет безопасные значения по умолчанию и намеренные точки расширения.

Он поддерживает тестирование без запуска реальной инфраструктуры в каждом тесте сервиса.

Он может раскрывать здоровье, не путая сбой зависимости с живостью процесса.

Он может поддерживать несколько настроенных экземпляров без глобальных реестров.

И всё это он может делать, сохраняя базовую технологию узнаваемой.

Хороший модуль Elasticsearch должен всё ещё ощущаться как Elasticsearch.

Хороший модуль NATS должен всё ещё ощущаться как NATS.

Хороший модуль MinIO должен всё ещё ощущаться как S3-совместимое объектное хранилище.

Хороший модуль внутренней библиотеки должен всё ещё раскрывать концепции, вокруг которых построена эта библиотека.

Kora поставляет производственный клей:

конфигурацию
+
DI
+
жизненный цикл
+
телеметрию
+
тестирование

а не заменяющую вселенную.

Именно так сфокусированный фреймворк может оставаться маленьким, не становясь закрытым.

Kora не обязана интегрировать каждую возможную библиотеку сама. Ей нужно раскрывать те же архитектурные примитивы достаточно сильно, чтобы команды могли строить собственные интеграции без обходных путей.

Идеальный конечный результат почти скучен.

Приложение включает модуль.

Конфигурация появляется под предсказуемым разделом.

Граф конструирует клиент.

Клиент запускается в корректном порядке зависимостей.

Операции испускают те же метрики и трассировки, что и остальной сервис.

Завершение освобождает ресурсы в корректном порядке.

Тесты заменяют клиент, когда нужна изоляция, и запускают реальную инфраструктуру, когда нужен реализм.

Бизнес-код просто внедряет технологию и использует её.

Именно так должен ощущаться первоклассный модуль.

И именно таков практический смысл фреймворка, созданного для расширения.