Как построить собственный первоклассный модуль 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 поставляет конфигурацию, жизненный цикл, телеметрию и связку
Это правильный способ понимать модель расширяемости фреймворка.
Модуль — это больше, чем фабричный метод¶
Начнём с вымышленной библиотеки:
Наивный модуль мог бы быть таким:
Технически это интегрирует клиент с DI. Операционно это слабо. Эндпоинт и секрет захардкожены, жизненный цикл не управляется, телеметрии не существует, несколько экземпляров невозможны, тестирование требует ручной замены реализации, и нет явного производственного контракта.
Первоклассная интеграция должна решать реальный жизненный цикл технологии, а не просто конструирование.
Полезный базовый уровень:
В зависимости от технологии производственному модулю могут также понадобиться:
6. Пробы
7. Поддержка нескольких экземпляров
8. Корректное завершение работы
9. Хуки отказоустойчивости
10. Поддержка native-image
11. Утилиты тестирования
12. Сгенерированные адаптеры
Не каждой библиотеке нужна каждая функция. Маленькому форматтеру без состояния может понадобиться только фабрика. Интеграции потребителя NATS нужно значительно больше. Модуль должен моделировать реальную операционную природу технологии.
Модули Kora принадлежат графу этапа компиляции¶
Граф зависимостей Kora в значительной степени валидируется во время компиляции. Поэтому модуль — не просто хук регистрации времени выполнения. Он становится частью той же архитектуры этапа компиляции, что и контроллеры, репозитории, клиенты, компоненты телеметрии и другие зависимости приложения.
Внутри единицы компиляции приложения интерфейс, аннотированный @Module, может предоставлять фабричные методы:
Параметры — это зависимости графа. Если у SearchClient нет провайдера или существует несколько неоднозначных провайдеров, Kora может отклонить приложение до запуска.
Это важно для авторов расширений, потому что им не нужно строить собственный контейнер или реестр времени выполнения. Они описывают компоненты и отношения; Kora компилирует структуру.
Внешние модули подключаются явно¶
Для переиспользуемых модулей, поставляемых в отдельном артефакте, модель Kora намеренно явная. Приложение включает внешний модуль через свой интерфейс @KoraApp, а не через сканирование classpath.
Концептуально:
а в сервисе:
Это различие важно.
Добавление JAR в classpath не автоматически означает:
инициализировать его клиент
запустить его потоки
подключиться к его удалённой системе
зарегистрировать все его компоненты
Приложение явно говорит:
Это делает интерфейс @KoraApp архитектурным манифестом.
Сервис может читаться так:
Инженер может немедленно увидеть, какие семейства инфраструктуры включены. Компилятор видит то же самое. нейро-агент видит то же самое.
Это сильнее скрытой авто-конфигурации.
Начните с тонкой интеграционной границы¶
Первый вопрос дизайна — не «Какую абстракцию фреймворка я могу изобрести?»
Он такой:
Что нативная библиотека уже делает хорошо, и какую производственную интеграцию Kora должна добавить вокруг неё?
Предположим, внешняя библиотека уже раскрывает хороший SearchClient. Модуль может просто раскрыть этот клиент напрямую:
Не добавляйте сразу:
если только эти абстракции не решают повторяющиеся проблемы.
Интеграция фреймворка и абстракция технологии — разные задачи.
Интеграция фреймворка отвечает:
Как это настраивается?
Кто это создаёт?
Кто это закрывает?
Как это наблюдается?
Как это заменяется в тестах?
Как сосуществуют несколько экземпляров?
Абстракция технологии отвечает:
Должен ли код приложения вообще видеть нативную библиотеку?
Должны ли мы раскрывать доменные репозитории?
Должны ли мы изолировать API вендора?
Первое обычно необходимо.
Второе опционально.
Первоклассный модуль не должен навязывать большой специфичный для фреймворка API, когда нативная библиотека уже хороша.
Шаг 1: Моделируйте конфигурацию как тип¶
Производственная интеграция обычно должна начинаться с типизированной конфигурации.
Например:
Конфигурация может выглядеть так:
Интерфейс конфигурации описывает форму и семантику конфигурации модуля.
Для переиспользуемого кода библиотеки полезно держать сам тип независимым от одного жёсткого пути конфигурации. Фабрика модуля может выбирать, откуда маппится эта форма.
Это допускает переиспользование, например:
без копирования одной и той же модели конфигурации.
Конфигурация библиотеки должна быть независимой от пути¶
Есть два отдельных контракта:
Тип отвечает:
Какие значения существуют?
Какие обязательны?
Каковы их типы Java?
Какие значения по умолчанию применяются?
Фабрика модуля отвечает:
Это разделение особенно полезно, когда общий модуль потребляется разными проектами или несколькими именованными экземплярами.
Оно также предотвращает хардкод иерархии конфигурации одной организации в переиспользуемой интеграции.
Избегайте сырой конфигурации глубоко в модуле¶
Можно внедрять сырую конфигурацию и снова и снова писать:
но это ослабляет модуль.
Строковые пути расползаются по коду конструирования. Значения по умолчанию и валидация разбрасываются. Конфигурация становится труднее тестируемой. Поведение обновления может непреднамеренно затрагивать большую часть графа.
Предпочитайте такую структуру:
Сырая конфигурация обычно должна оставаться на границе.
Типизированная конфигурация становится реальным контрактом модуля.
Конфигурация — это публичный API¶
Как только модуль становится общим, его конфигурация так же важна, как его Java API.
Изменение:
на:
может быть ломающим изменением развёртывания, даже если каждый класс Java всё ещё компилируется.
Значения по умолчанию тоже важны. Изменение таймаута по умолчанию с пяти секунд на шестьдесят меняет производственное поведение.
Первоклассный модуль должен документировать и версионировать:
имена свойств
типы
единицы
значения по умолчанию
обязательные значения
значения, чувствительные к безопасности
семантическое значение
Конфигурация — не деталь реализации.
Значения по умолчанию должны быть безопасными для продакшена¶
Избегайте удобных, но опасных значений по умолчанию, таких как:
бесконечный таймаут
неограниченный пул
отключённая проверка TLS
автоматическое создание разрушительных ресурсов
неограниченный повтор
Предпочитайте ограниченные, явные значения по умолчанию:
конечные таймауты
ограниченные соединения
безопасный TLS
консервативный повтор
предсказуемое завершение
Переиспользуемый модуль централизует производственное поведение во многих сервисах. Плохие значения по умолчанию масштабируются так же эффективно, как и хорошие.
Шаг 2: Сконструируйте клиент через DI¶
Как только конфигурация существует, создайте клиент, используя обычные зависимости графа:
Бизнес-компоненты теперь зависят от обычного типа:
Нет статического реестра.
Нет сервис-локатора.
Нет поиска во время выполнения.
Граф зависимостей говорит ровно то, что требуется приложению.
Граф должен раскрывать реальные инфраструктурные зависимости¶
Предположим, интеграции также нужны:
Сделайте эти зависимости видимыми:
Это даёт Kora полезную информацию.
Граф может валидировать полноту.
Тесты могут заменять отдельные части.
Порядок жизненного цикла может следовать реальным отношениям зависимостей.
Сгенерированная связка становится читаемой.
Скрытые глобальные поиски уничтожают эти преимущества.
Внедрение зависимостей на этапе компиляции — страховочная сеть автора модуля¶
Предположим, ваш модуль требует:
Если приложение забывает провайдер учётных данных, это должно стать ошибкой построения графа, а не поздним сюрпризом времени выполнения.
Это одна из самых сильных причин реализовывать инфраструктуру как реальные модули Kora.
Автору модуля не нужна кастомная валидация запуска для базового существования зависимостей и неоднозначности. Kora уже предоставляет эту структурную проверку.
Шаг 3: Относитесь к жизненному циклу как к части API¶
Инфраструктурные клиенты часто владеют ресурсами:
TCP-соединения
потоки event-loop
пулы воркеров
подписки
heartbeat'ы
буферы
фоновые задачи обновления
Создание объекта — лишь половина проблемы.
Интеграция должна ответить:
Когда он готов?
Кто его запускает?
Кто его останавливает?
Кто закрывает сетевые ресурсы?
Что происходит при частичном сбое запуска?
У Kora есть явная семантика жизненного цикла компонентов.
Компонент может реализовывать:
Для внешних типов, которые не могут реализовывать интерфейсы Kora, фабрика может прикрепить жизненный цикл через обёртку.
Это ровно то, для чего существует интеграционный слой: сохранение внешнего API при подключении жизненного цикла его ресурсов к графу приложения.
Используйте AutoCloseable, когда его достаточно¶
Если нативный клиент уже реализует AutoCloseable, не изобретайте параллельную абстракцию жизненного цикла только ради согласованности.
Граф может закрывать принадлежащие ресурсы во время release.
Предпочитайте нативные контракты, когда они корректно представляют владение.
Хорошая интеграция Kora остаётся близко к базовой технологии.
Используйте жизненный цикл, когда запуск имеет реальное значение¶
Интеграции NATS может понадобиться:
подключиться
зарегистрировать подписки
инициализировать потребителей JetStream
ждать готовности диспетчера
Клиенту MinIO может не понадобиться значимая инициализация сверх конструирования.
Транспорту Elasticsearch может понадобиться создать ресурсы соединения, но не выполнять блокирующий вызов здоровья кластера.
Не заставляйте каждый модуль выполнять сетевую валидацию при запуске только потому, что жизненный цикл существует.
Каждый обязательный удалённый вызов при запуске увеличивает время до готовности и делает развёртывания более чувствительными к внешним сбоям.
Модуль должен тщательно различать:
LifecycleWrapper полезен для сторонних объектов¶
Фабрика может концептуально возвращать обёрнутый клиент:
Остальная часть приложения по-прежнему внедряет SearchClient.
Жизненный цикл остаётся задачей интеграции.
Это сильный паттерн для адаптации библиотек, которые ничего не знают о Kora.
Обратное освобождение зависимостей ценно¶
Предположим:
Во время завершения работы зависимый компонент должен остановиться первым, затем может закрыться клиент.
Kora освобождает компоненты графа в обратном порядке зависимостей.
Это значит, что корректное моделирование графа естественно производит разумное завершение ресурсов.
Это ещё одна причина, почему скрытые зависимости вредны: граф не может упорядочить ресурсы, которых не видит.
Корректное завершение работы определяет качество интеграции¶
Обмен сообщениями делает это особенно очевидным.
Модуль потребителя NATS должен думать о:
прекращении приёма новых сообщений
завершении или отмене обработчиков в полёте
сбросе публикаций
сливе подписок
закрытии соединения
Точное поведение зависит от NATS и политики приложения.
Но просто немедленно закрыть сокет во время каждой раскатки недостаточно для серьёзной интеграции.
Клиент может быть полностью корректным в установившемся режиме и всё же быть операционно плохим, если каждое развёртывание теряет работу.
Первоклассные модули интегрируют семантику завершения.
Избегайте ненужной работы при запуске¶
Kora инициализирует независимые компоненты графа параллельно, где возможно.
Кастомный модуль может легко стереть это преимущество, выполняя несвязанную последовательную работу в одной фабрике:
Отделяйте обязательную инициализацию от опционального прогрева.
Не выполняйте работу развёртывания/плоскости управления во время обычного запуска приложения, если она не обязательна.
Производительность запуска фреймворка зависит и от авторов модулей.
Шаг 4: Дайте модулю контракт телеметрии¶
Производственный модуль должен ответить:
Как минимум подумайте о:
Полезный дизайн — специфичная для модуля абстракция телеметрии:
Затем каждая операция следует одному жизненному циклу:
Это зеркалит более широкую архитектуру телеметрии Kora: контекст, специфичный для операции, вокруг реальной работы.
Один контекст наблюдения лучше трёх независимых обёрток¶
Слабая реализация может вручную делать:
в каждом методе.
Лучшая структура:
и один контекст операции обрабатывает симметрию start/end.
Это централизует семантику успеха/сбоя и делает no-op телеметрию дешёвой, когда она отключена.
Телеметрия должна описывать нативную технологию¶
Полезные измерения в стиле Elasticsearch могут включать:
Для NATS:
Для MinIO:
Не прячьте это за специфичным для фреймворка словарём, если операторы уже понимают технологию.
В то же время кардинальность должна контролироваться.
Ключ объекта — обычно ужасная метка метрики.
Subject NATS может быть безопасным, если ограничен, и опасным, если генерируется динамически.
Индекс Elasticsearch может быть ограничен в одной архитектуре и неограничен в другой.
Знания технологии всё равно требуются.
Метрикам нужны ограниченные измерения¶
Метрики — инструменты агрегации.
Хорошие измерения часто включают:
Плохие измерения часто включают:
идентификатор запроса
идентификатор пользователя
ключ объекта
сырой запрос
полное сообщение исключения
Первоклассный модуль должен выбирать безопасные значения по умолчанию, чтобы потребляющие сервисы не открывали правила кардинальности заново независимо.
Трассировки могут нести более богатый контекст¶
Трассировка обычно может терпеть более богатые метаданные уровня запроса, чем метрики.
Span поиска может включать:
не добавляя полное тело запроса.
Span публикации NATS может включать subject и размер полезной нагрузки без копирования содержимого сообщения.
Span загрузки в MinIO может включать bucket и размер, избегая чувствительных ключей.
Телеметрия должна быть полезной, не становясь утечкой данных.
Логи должны фиксировать исключительные состояния, а не каждый успех¶
Полезные логи модуля включают:
переподключение NATS началось
переподключение NATS завершено
узел Elasticsearch помечен недоступным
многокомпонентная загрузка MinIO прервана
обновление учётных данных не удалось
Модулю обычно не нужен INFO-лог для каждого успешного запроса, если метрики и трассировка уже покрывают установившееся поведение.
Структурированное логирование должно объяснять необычные решения и переходы состояний.
Переиспользуйте существующий стек наблюдаемости Kora¶
Кастомная интеграция обычно должна зависеть от существующей инфраструктуры метрик, трассировки и логирования приложения, а не изобретать ещё один стек экспортёров.
Это даёт одному сервису:
вместо другой системы мониторинга для каждой библиотеки.
Именно это заставляет кастомный модуль операционно ощущаться как встроенная интеграция Kora.
Следуйте знакомым конвенциям конфигурации телеметрии¶
Если встроенные модули Kora используют такие концепции, как:
кастомный модуль должен серьёзно рассмотреть похожую форму.
Например:
Разработчики, уже знакомые с Kora, немедленно это поймут.
Первоклассный означает связный с фреймворком, а не просто совместимый с ним.
Решите, раскрывать ли нативный клиент или тонкий адаптер¶
Есть два хороших дизайна.
Нативный клиент как компонент¶
Это идеально, когда сторонний клиент уже раскрывает полезные хуки для транспорта, телеметрии или жизненного цикла.
Тонкий инструментированный адаптер¶
Это полезно, когда телеметрию или политику нельзя чисто прикрепить к нативному клиенту.
Обёртка должна заслуживать своё существование.
Если она только переименовывает:
она даёт мало ценности.
Полезная обёртка должна вносить что-то реальное:
Конкретная архитектура модуля поиска¶
Разумная архитектура может быть:
SearchConfig
↓
CredentialsProvider
↓
SearchTransport
↓
SearchTelemetry
↓
SearchClient
↓
CatalogSearchService
Типизированная конфигурация:
Простой адаптер телеметрии:
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
}
}
}
Граф приложения обрабатывает конструирование.
Бизнес-код видит обычный типизированный клиент.
Держите фабричные методы маленькими¶
Избегайте гигантской фабрики, такой как:
Предпочитайте меньшие узлы графа:
Внедрение зависимостей на этапе компиляции хорошо собирает компоненты.
Позвольте Kora делать эту работу, а не создавать частный мини-контейнер внутри одного метода.
Проектируйте явные точки переопределения¶
Общий модуль может предоставлять:
но некоторым приложениям понадобятся замены.
Хорошие точки расширения часто включают:
Не раскрывайте каждый внутренний объект как публичный API.
Раскрывайте стабильные границы там, где кастомизация действительно полезна.
Это предотвращает форки без замораживания деталей реализации.
Заменяемость — часть «создан для расширения»¶
Расширяемость — это не только способность добавлять новые компоненты.
Это также способность заменять значения по умолчанию.
Общий модуль поиска может по умолчанию использовать статические учётные данные API-ключа, в то время как одна производственная платформа использует workload identity.
Модуль должен допускать эту замену через DI.
То же самое применимо к телеметрии, TLS, транспорту и выбранному поведению конструирования клиента.
Первоклассный модуль переиспользуем, потому что предоставляет сильные значения по умолчанию, не будучи жёстким.
Несколько экземпляров требуют намеренного дизайна¶
Приложения часто перерастают допущение об одном глобальном клиенте.
Примеры:
основной поисковый кластер
аналитический поисковый кластер
NATS плоскости управления
NATS плоскости событий
внутреннее объектное хранилище
внешнее объектное хранилище
Паттерны тегов и фабричных модулей Kora могут представлять несколько независимо настроенных экземпляров.
Автор модуля должен сознательно выбирать, поддерживается ли многоэкземплярное использование.
Не делайте его случайно невозможным, пряча всё состояние в одном статическом синглтоне.
Фабричные модули помогают параметризованным интеграциям¶
Иногда одна переиспользуемая реализация модуля должна параметризоваться путём конфигурации или логической идентичностью.
Концептуально:
может создавать два независимых набора:
с тегами, различающими их.
Это мощная функция, потому что кастомная инфраструктура может использовать те же примитивы графа, которые Kora использует для собственных параметризованных модулей.
Сторонние интеграции не ограничены более слабой моделью расширения.
Теги должны выражать смысл¶
Теги уместны, когда два компонента имеют один тип, но разные семантические роли:
Это передаёт архитектуру.
Теги менее полезны, когда добавляются произвольно для исправления плохо спроектированной неоднозначности графа.
Тег должен говорить читателю, почему компоненты различаются.
Elasticsearch: что нужно первоклассному модулю¶
Хороший модуль Elasticsearch должен, вероятно, интегрировать:
типизированную конфигурацию
учётные данные
TLS
транспорт HTTP
нативный клиент
жизненный цикл клиента/транспорта
телеметрию
опциональную информацию о здоровье
поддержку тестирования
Концептуальный граф:
Нативный API Elasticsearch должен оставаться видимым, если организация намеренно не хочет стабильную внутреннюю абстракцию.
Телеметрию может быть лучше всего прикреплять на уровне транспорта, если нативный клиент предоставляет подходящие хуки. Обёртывание каждого высокоуровневого вызова API вручную может быть хрупким.
Цель — использовать нативные точки расширения, где возможно, и добавлять интеграцию Kora вокруг них.
Не двойной инструментируйте Elasticsearch¶
Современные клиенты и транспорты могут уже раскрывать инструментацию, совместимую с OpenTelemetry.
Если нативная библиотека уже создаёт полезный span клиента, а модуль Kora создаёт идентичный span вокруг него, трассировки становятся шумными:
Поймите модель наблюдаемости библиотеки, прежде чем добавлять ещё один слой.
Первоклассный модуль компонуется с нативной инструментацией, а не слепо дублирует её.
NATS: жизненный цикл — главная задача¶
Интеграция NATS может включать:
соединение
состояние переподключения
контекст JetStream
подписки
диспетчеры
обработчики сообщений
слив
сброс
завершение
Возможный граф:
Здесь жизненный цикл гораздо важнее простого конструирования.
Менеджеру потребителей может понадобиться запускать подписки после готовности соединения и сливать их до закрытия соединения.
Телеметрия должна покрывать операции публикации и обработку сообщений.
Хорошая интеграция должна операционно раскрывать поведение переподключения и потребителя.
Компоненты с побочными эффектами могут нуждаться в статусе корневых¶
Менеджер потребителей может существовать только потому, что его жизненный цикл запускает подписки.
Никакой другой компонент не обязательно его внедряет.
В графе зависимостей, управляемом спросом, такой компонент может исчезнуть, если ничто не тянет его в граф.
Инфраструктура, само существование которой является поведением приложения, может нуждаться в пометке как корневой компонент.
Примеры включают:
Не помечайте всё как корневое.
Используйте статус корневого для компонентов, чьи побочные эффекты намеренно являются частью работающего приложения.
MinIO: сохраняйте семантику объектного хранилища видимой¶
Модуль MinIO обычно ближе к клиентскому модулю.
Конфигурация может включать:
Телеметрия может описывать:
get_object
put_object
remove_object
stat_object
bucket
длительность
размер полезной нагрузки
результат
Избегайте таких измерений метрик, как сырой ключ объекта.
Не создавайте автоматически bucket'ы при каждом запуске приложения, если это не явная функция плоскости управления.
Сервис, использующий MinIO для операций плоскости данных, не должен автоматически требовать привилегий администрирования bucket'ов.
Отделяйте плоскость управления от плоскости данных¶
Этот принцип применим во всех технологиях.
Примеры действий плоскости управления:
создать индекс Elasticsearch
установить шаблоны
создать поток NATS
изменить срок хранения
создать bucket объектного хранилища
Примеры действий плоскости данных:
Реплики приложения обычно нуждаются в плоскости данных.
Им могут не понадобиться административные привилегии.
Первоклассный модуль должен избегать смешивания работы развёртывания/настройки в каждый запуск сервиса, если архитектура явно этого не требует.
Внутренняя библиотека X использует тот же паттерн¶
Предположим, компания владеет:
Ему нужны типизированная конфигурация, TLS, учётные данные, телеметрия, обновление модели и корректное завершение.
Модуль Kora может раскрывать:
Паттерн идентичен.
Именно здесь модульность фреймворка становится организационно ценной: специфичная для компании инфраструктура может участвовать в той же модели приложения, что и встроенные интеграции.
Держите публичную поверхность модуля маленькой¶
Как только несколько сервисов зависят от модуля, каждый публичный тип становится обещанием совместимости.
Раскрывайте только то, что действительно нужно потребителям и авторам расширений.
Полезная форма может быть:
Детали реализации должны оставаться приватными.
Это облегчает обновления и держит модуль понятным.
Отделяйте основную библиотеку от интеграции Kora¶
Если вы владеете библиотекой X, рассмотрите:
вместо того чтобы заставлять саму основную библиотеку зависеть от Kora.
Тогда основную библиотеку можно использовать:
а модуль Kora действует как адаптер фреймворка.
Это сохраняет ответственности чистыми.
Тонкозернистые зависимости важны¶
Кастомный модуль должен зависеть только от тех возможностей Kora, которые реально использует.
Если интеграции MinIO нужны конфигурация и телеметрия, она не должна тянуть HTTP-сервер, базу данных, Kafka и несвязанные модули.
Аналогично, опциональные утилиты тестирования не должны быть производственными зависимостями.
Это следует более широкому принципу Kora:
Раздутая интеграция может подрывать модульный рантайм фреймворка.
Опциональные функции должны быть действительно опциональными¶
Предположим, трассировка отключена.
Модуль должен уметь использовать дешёвый no-op путь телеметрии вместо требования ненужного поведения экспортёра.
Предположим, пробы не используются.
Клиент не должен запускать фоновый проверщик здоровья только потому, что интеграция его поддерживает.
Опциональные функции не должны молча тянуть большие подграфы рантайма в каждое приложение.
Пробы — это решение политики¶
Должен ли недоступный кластер Elasticsearch делать приложение неготовым?
Иногда да.
Иногда нет.
Сервис, который не может выполнять никакую полезную работу без поиска, может обоснованно проваливать готовность.
Сервис, который может откатиться на PostgreSQL, вероятно, не должен убирать себя из трафика только потому, что поиск лежит.
Поэтому переиспользуемый модуль обычно должен раскрывать информацию о здоровье, не навязывая универсальную семантику готовности или живости.
Приложение решает, как здоровье зависимостей отображается на допуск трафика.
Не превращайте сбой удалённой зависимости в сбой живости¶
Перезапуск приложения редко чинит недоступный внешний сервис.
Отключение NATS, отключение Elasticsearch или отключение MinIO не должно автоматически заставлять каждую реплику сервиса проваливать живость и перезапускаться.
Это может превратить один инфраструктурный инцидент в шторм перезапусков всего флота.
Дизайн проб принадлежит архитектуре модуля, а не только конфигурации фреймворка.
Проверки здоровья должны быть дешёвыми¶
Проба не должна выполнять дорогой поиск или загрузку объекта каждую секунду из каждого Pod.
Это создаёт производственную нагрузку от самого мониторинга.
Предпочитайте:
нативный лёгкий эндпоинт здоровья
состояние соединения
ограниченный ping
кешированное состояние здоровья
в зависимости от технологии.
Наблюдаемость не должна становиться атакой на зависимость.
Обновление конфигурации требует намеренной политики¶
Kora может обновлять затронутые компоненты графа при изменении конфигурации.
Автор модуля должен спросить:
Можно ли безопасно пересоздать этот клиент?
Должны ли потребители тоже пересоздаваться?
Должен ли какой-то долгоживущий компонент наблюдать новый клиент косвенно?
Требует ли это изменение перезапуска процесса?
Не наследуйте поведение обновления случайно.
Внешние клиенты могут владеть сокетами, подписками, потоками или дорогим состоянием.
Перезагрузка конфигурации мощна и должна отражать семантику ресурсов.
Прямые зависимости и ValueOf означают разную связанность жизненного цикла¶
Прямая зависимость графа означает, что потребитель связан по жизненному циклу с зависимостью.
Косвенная зависимость ValueOf<T> может позволить долгоживущему компоненту наблюдать текущее значение без пересборки при каждом изменении этой зависимости.
Это полезно для инфраструктуры, которая должна оставаться работающей, пока одна из её зависимостей обновляется.
Но не используйте косвенность просто для избежания корректного отношения зависимости.
Используйте её, когда живая замена — реальное архитектурное требование.
Тестирование — часть дизайна модуля¶
Первоклассный модуль должен быть тестируем на нескольких уровнях.
Модульные тесты модуля¶
Валидируйте:
Интеграционные тесты модуля¶
Используйте реальную инфраструктуру там, где важна семантика протокола:
Тесты приложения-потребителя¶
Позвольте приложению заменять реальный клиент фейковой или тестовой реализацией.
Каждый уровень отвечает на разные вопросы.
Замена компонентов — основной тестовый шов¶
Тест сервиса должен уметь заменять:
на:
без запуска Elasticsearch.
Затем отдельный интеграционный набор упражняет реальный модуль клиента.
Это здоровое разделение:
DI должен делать это лёгким.
Если модуль затрудняет замену, его интеграционная граница, вероятно, слишком жёсткая.
Реальные интеграционные тесты всё равно необходимы¶
Моки не могут доказать:
поведение TLS
аутентификацию
совместимость протокола
семантику запросов Elasticsearch
подтверждение NATS
многокомпонентное поведение MinIO
нативный повтор
сетевой таймаут
завершение
Самому модулю нужно реальное интеграционное покрытие.
Testcontainers или эквивалентная одноразовая инфраструктура идеальны, где доступны.
Быстрый запуск графа Kora помогает делать такие тесты практичными.
Тестируйте жизненный цикл, а не только бизнес-операции¶
Тест модуля должен проверять больше, чем:
Также тестируйте:
сбой запуска
очистку при частичном запуске
завершение
повторные запуск/остановку, где релевантно
обработку операций в полёте
закрытие ресурсов
Многие интеграционные баги появляются только во время раскатывающегося развёртывания или частичного сбоя.
Первоклассным модулям нужны тесты жизненного цикла.
Тестируйте семантику телеметрии¶
Проверяйте:
метрика успеха испускается один раз
классификация сбоя стабильна
span закрывается при исключении
секреты не логируются
имена операций остаются ограниченными
Дашборды и алерты могут зависеть от этих контрактов.
Телеметрия — часть API модуля.
Инъекция сбоев важна¶
Инфраструктурные модули должны тестировать:
отказ соединения
сбой аутентификации
таймаут
ошибку удалённого сервера
отключение брокера
недопустимую конфигурацию
завершение во время работы
и проверять:
Производственное качество модуля во многом определяется его путём сбоя.
Ошибки запуска должны быть действенными¶
Если инициализация падает, ошибка должна говорить разработчику:
Избегайте расплывчатой обёртки, такой как:
Kora может показывать сбой инициализации графа, но модуль должен сохранять достаточно деталей, чтобы ошибка графа была полезной.
Частичная инициализация требует очистки¶
Рассмотрим:
Интеграция должна освободить A и соединение.
Код жизненного цикла, предполагающий, что запуск всегда завершается, может утекать ресурсы.
Простые клиенты могут использовать простые обёртки.
Сложные системы подписок могут заслуживать выделенного менеджера с явным состоянием.
Отделяйте менеджеров, когда ответственности различаются¶
Архитектура NATS может выигрывать от:
Соединение владеет транспортом.
Менеджер владеет подписками и жизненным циклом обработки сообщений.
Во время завершения:
Граф естественно представляет этот порядок.
Не перегружайте один компонент несвязанными ответственностями только для минимизации количества классов.
Владение ресурсами должно быть явным¶
Для каждого ресурса ответьте:
Например:
транспорт HTTP поиска
создаётся модулем
принадлежит модулю
закрывается модулем
трейсер OpenTelemetry
создаётся приложением/платформой
внедряется в модуль
не закрывается модулем
Компонент никогда не должен закрывать общую зависимость, которой не владеет.
Владение — часть дизайна жизненного цикла.
Избегайте скрытых пулов потоков¶
Сторонние клиенты могут создавать собственные воркеры или event-loop'ы.
Если модуль поддерживает несколько экземпляров клиента, количество потоков может неожиданно умножиться.
Документируйте важное поведение ресурсов.
Где безопасно и поддерживается, позволяйте внедрять общие исполнители или транспорты.
Не прячьте дорогие фоновые ресурсы за маленько выглядящей фабрикой клиента.
Потоки должны уважать нативную библиотеку¶
Некоторые клиенты синхронны.
Некоторые асинхронны.
Некоторые управляют собственными event-loop'ами.
Модель виртуальных потоков Kora 2 означает, что синхронный блокирующий клиент часто может оставаться синхронным без реактивной обёртки.
Не добавляйте автоматически исполнители или асинхронные адаптеры ради эстетической согласованности.
Тонкая интеграция означает уважение нативной модели исполнения.
Отказоустойчивость должна быть компонуемой, а не скрыто захардкоженной¶
Должен ли каждый поиск повторяться три раза?
Должна ли каждая загрузка объекта использовать circuit breaker?
Должна ли каждая публикация NATS автоматически повторяться?
Универсального ответа нет.
Предпочитайте:
нативный повтор транспорта, где уместно
+
политику отказоустойчивости приложения/Kora, где семантически уместно
вместо скрытия агрессивного поведения внутри конструирования клиента.
Модуль должен документировать нативную семантику повтора, чтобы повтор уровня приложения случайно не умножал попытки.
Усиление повторов легко создать¶
Предположим:
Система может выполнить до девяти попыток.
При сбое зависимости это может резко усилить нагрузку.
Телеметрия модуля должна делать попытки и сбои видимыми, где возможно.
Документация должна объяснять нативное поведение.
Интеграция фреймворка должна уменьшать сюрпризы, а не создавать их.
Слои таймаутов требуют той же осторожности¶
Нативный таймаут запроса и внешний таймаут Kora не обязательно одно и то же.
Например:
Если отмена слабая, приложение может перестать ждать, в то время как базовая операция продолжает потреблять ресурсы.
Первоклассный модуль должен документировать поведение отмены и таймаутов базовой библиотеки.
Операционная корректность зависит от этого.
Конфигурация безопасности должна быть явной¶
Учётные данные не должны извлекаться из случайных переменных среды внутри глубокого кода реализации.
Предпочитайте ясную зависимость:
или типизированную конфигурацию секретов.
Это позволяет приложениям выбирать:
без изменения бизнес-кода.
Отделение получения учётных данных от конструирования клиента — отличный шов расширения.
TLS — часть интеграционного контракта¶
Серьёзным модулям нужно учитывать:
Не делайте небезопасный TLS лёгкой конфигурацией.
Если у организации есть общие TLS-компоненты, интегрируйтесь с ними.
Иначе раскрывайте ясные типизированные опции.
Безопасность — не запоздалая мысль к конструированию клиента.
Никогда не утекайте секреты через телеметрию¶
Модуль видит учётные данные и потенциально чувствительные данные запросов.
Он должен предотвращать случайное логирование:
API-ключа
заголовка Authorization
пароля
секретного access key
подписанного URL
токена учётных данных
Маскирование принадлежит общей интеграции, чтобы каждый сервис не изобретал его заново.
Централизованный код модуля — множитель силы и для хорошей, и для плохой безопасности.
Начните с DI, прежде чем строить генерацию кода¶
Kora широко использует процессоры аннотаций, но кастомному модулю автоматически не нужен собственный.
Разумная последовательность:
Фаза 1:
фабрики модуля
Фаза 2:
типизированная конфигурация + жизненный цикл
Фаза 3:
телеметрия + тесты
Фаза 4:
поддержка нескольких экземпляров
Фаза 5:
генерация, только если повторяющиеся статические контракты её оправдывают
Не стройте процессор только потому, что у самой Kora есть процессоры.
Используйте генерацию кода, когда есть реальная структура этапа компиляции, которую стоит генерировать.
Первоклассный модуль может иметь почти нулевой API Kora в точке использования¶
Это часто идеальный результат.
Сам модуль использует:
но бизнес-код просто внедряет:
и вызывает его.
Это значит, что интеграция фреймворка удалась без загрязнения обычного использования технологии.
Kora богата на границе сборки и тонка в модели программирования рантайма.
Сравните тонкую интеграцию со стеком обёрток¶
Переусложнённая интеграция может стать:
Каждый слой добавляет маппинг, исключения, конфигурацию и телеметрию.
Это выглядит «полным», но увеличивает поверхность обучения, время отладки и стоимость обновления.
Тонкая интеграция:
оставляет технологию узнаваемой.
Тонкие модули обновляются легче¶
Когда меняется нативный клиент, тонкий модуль в основном обновляет:
Большой специфичной для фреймворка абстракции может понадобиться переводить множество функций вендора между версиями.
Низкая семантическая дистанция снижает стоимость обслуживания.
Это особенно важно для инфраструктурных библиотек, которые быстро развиваются.
Политика версионирования важна¶
Общий модуль должен документировать:
поддерживаемую версию нативного клиента
совместимость с Kora
допущения управления зависимостями
раскрывается ли зависимость нативного клиента
Не существует одной правильной схемы версионирования.
Но политика должна быть намеренной.
Слишком большая гибкость может производить несовместимые комбинации зависимостей.
Слишком жёсткая фиксация может блокировать обновления безопасности.
Конфликты зависимостей — это инженерия модуля¶
Инфраструктурные клиенты часто зависят от:
Переиспользуемый модуль должен сосуществовать с более широким стеком Kora.
Избегайте ненужного shading или приватных копий фундаментальных библиотек.
Выравнивайте через управление зависимостями, где это практично.
Первоклассная интеграция — не только API-клей; это также гигиена зависимостей.
Схема телеметрии модуля — это API¶
Изменение метрики с:
на:
может сломать дашборды.
Изменение меток может сломать запросы алертов.
Изменение именования спанов может сломать поиск по трассировкам.
Совместимость телеметрии важна, когда модуль широко развёрнут.
Версионируйте её намеренно.
Модуль становится примитивом организационной платформы¶
Первый сервис может интегрировать библиотеку X локально.
Пятый сервис, вероятно, не должен снова копировать ту же настройку.
Общий модуль Kora может централизовать:
конструирование
таймауты
TLS
учётные данные
телеметрию
завершение
версии зависимостей
поддержку тестирования
Каждый сервис получает накопленные инфраструктурные знания.
Именно здесь модульность становится механизмом организационного масштабирования.
Не извлекайте общие модули слишком рано¶
Один сервис с одной фабрикой не обязательно оправдывает артефакт платформы.
Полезная эволюция:
локальная интеграция
↓
второй реальный потребитель
↓
определить стабильное общее поведение
↓
извлечь переиспользуемый модуль
Преждевременные инфраструктурные абстракции часто замораживают не тот API.
Позвольте реальному использованию раскрыть стабильную интеграционную границу.
Первоклассный не значит официальный¶
Модуль компании может быть таким же первоклассным, как интеграция, поддерживаемая Kora, если он корректно участвует в:
Это практический тест расширяемости.
Пользователям не должны требоваться модификации ядра Kora, чтобы строить продакшен-классные интеграции.
Компилятор — часть поверхности расширения¶
Объявляя фабрики модулей и типизированные зависимости, кастомные интеграции автоматически выигрывают от компилятора Kora:
обнаружение отсутствующих зависимостей
обнаружение неоднозначности
обнаружение циклов
сгенерированная связка
Автору модуля не нужен движок начальной загрузки рантайма.
Это мощное свойство.
Компилятор фреймворка становится инфраструктурой, общей для экосистемы.
Сгенерированная связка помогает отлаживать кастомные модули¶
Если интеграция ведёт себя странно, разработчики могут изучить сгенерированную связку приложения и увидеть:
Это особенно полезно для модулей уровня организации, поддерживаемых платформенной командой.
Потребители не вынуждены доверять невидимой авто-конфигурации.
Они могут изучить, что построила Kora.
Нейро-агенты выигрывают от явных модулей¶
нейро-агент может следовать:
затем изучить:
Если граф неполон, компиляция даёт точную обратную связь.
Магии classpath, которую нужно выводить, мало.
Это ровно тот вид архитектуры, который хорошо работает с разработкой инфраструктуры при поддержке агентов.
Сильные типы улучшают и DI, и нейро-технологии¶
Избегайте API модулей, в которых доминируют:
когда существует более сильный тип.
Предпочитайте:
Сильные контракты улучшают валидацию графа, навигацию в IDE, документацию и рассуждение с помощью нейро-технологий.
Качество типов напрямую влияет на качество инфраструктуры.
Избегайте API сервис-локатора¶
Kora поддерживает продвинутый доступ к графу для случаев, которые действительно в нём нуждаются.
Не используйте его для обычного конструирования.
Предпочитайте:
вместо:
Типизированные параметры зависимостей сохраняют валидацию этапа компиляции.
Используйте поиск по графу только когда динамическое поведение графа действительно часть проблемы.
Избегайте построения второго контейнера внутри фабрики¶
Если один метод модуля содержит сложные условные поиски, ручное кеширование синглтонов и динамическое конструирование, модуль воссоздаёт DI-контейнер внутри Kora.
Вместо этого выражайте архитектуру как компоненты графа, где практично.
Позвольте Kora разрешать зависимости.
Фабричные методы должны создавать значения, а не реализовывать скрытый фреймворк приложения.
Опциональные функции не должны тянуть неиспользуемую инфраструктуру¶
Если модуль поддерживает:
но приложению нужен только базовый клиент, оно не должно инициализировать всё.
Это рекурсивно применяет собственный принцип модульности Kora:
Кастомный модуль не должен становиться монолитом внутри модульного фреймворка.
Разделяйте большие интеграции, когда границы становятся реальными¶
Зрелая интеграционная экосистема может в итоге иметь:
или:
Делайте это только когда границы снижают зависимости или поведение рантайма.
Больше Gradle-модулей не автоматически означает лучшую модульность.
Разделение должно соответствовать значимым границам возможностей.
Утилиты тестирования могут жить отдельно¶
Производственный артефакт не должен зависеть от Testcontainers или большой фейковой инфраструктуры.
Поддержка тестирования может жить в выделенном артефакте:
Это сохраняет чистый производственный граф зависимостей, стандартизируя настройку тестов для потребителей.
Идеальный опыт разработчика¶
Потребитель готового модуля должен уметь сделать что-то близкое к:
предоставить:
и внедрить:
Разработчик приложения не должен вручную:
разбирать конфигурацию
строить транспорт
регистрировать хуки завершения
конструировать метрики
создавать спаны
маскировать учётные данные
закрывать ресурсы
Модуль владеет этими механизмами.
В то же время, если что-то идёт не так, фабрики и сгенерированный граф остаются инспектируемыми.
Эта комбинация — идеал Kora:
Идеальный опыт оператора¶
Оператор должен видеть кастомную интеграцию через те же инструменты, что и встроенные модули Kora:
операционные метрики
распределённые спаны
структурированные ошибки
логи жизненного цикла
состояние проб, где настроено
Не должно быть отдельной экосистемы наблюдаемости просто потому, что клиент пришёл из сторонней библиотеки.
Первоклассный означает и операционно нативный.
Идеальный опыт платформенной команды¶
Платформенная команда должна уметь публиковать:
и знать, что каждый сервис получает стандартизированные:
Именно здесь утверждение «создан для расширения» становится стратегически важным.
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 поставляет производственный клей:
а не заменяющую вселенную.
Именно так сфокусированный фреймворк может оставаться маленьким, не становясь закрытым.
Kora не обязана интегрировать каждую возможную библиотеку сама. Ей нужно раскрывать те же архитектурные примитивы достаточно сильно, чтобы команды могли строить собственные интеграции без обходных путей.
Идеальный конечный результат почти скучен.
Приложение включает модуль.
Конфигурация появляется под предсказуемым разделом.
Граф конструирует клиент.
Клиент запускается в корректном порядке зависимостей.
Операции испускают те же метрики и трассировки, что и остальной сервис.
Завершение освобождает ресурсы в корректном порядке.
Тесты заменяют клиент, когда нужна изоляция, и запускают реальную инфраструктуру, когда нужен реализм.
Бизнес-код просто внедряет технологию и использует её.
Именно так должен ощущаться первоклассный модуль.
И именно таков практический смысл фреймворка, созданного для расширения.