
В первой части я рассказал, что такое капсула, откуда берётся эта идея и зачем вообще упаковывать опыт. Если вы её не читали — советую начать оттуда, иначе дальнейшее может быть непонятным.
В этой части перейдём от теории к практике. Я покажу, как мы создавали собственный капсульный фреймворк для микросервисов, что закладывали в его основу и как он стал ДНК наших проектов.
Серия «Разработка через капсулы»:
Часть I: Опыт, которого нельзя потерять: что такое капсула и зачем она нужна
Часть II: Капсульный фреймворк: как мы упаковали архитектуру в ДНК проектов ← вы здесь
Часть III: Капсулы и AI-агенты: как передать опыт разработчика машине (скоро)
До фреймворка: как это было
Прежде чем рассказывать, что мы построили, стоит честно описать, от чего уходили.
У нас было несколько технически схожих проектов на микросервисах. Все они использовали TypeScript, все строились на похожих принципах, все общались через брокер сообщений. Но при ближайшем рассмотрении сходство заканчивалось на уровне технологий. Структура сервисов в каждом проекте своя. Один проект называл директорию controllers/, другой handlers/, третий methods/. В одном проекте сервисы общались через HTTP, в другом через NATS, в третьем — смешанно. DI-контейнер где-то был, где-то нет. Телеметрия в одном проекте подключена через middleware, в другом руками в каждом методе, в третьем отсутствует вовсе.
Но между разными проектами — это полбеды. Куда болезненнее была неоднородность внутри одного проекта. Микросервисная архитектура располагает к тому, что каждый сервис развивает свой разработчик. И каждый разработчик — художник, который видит мир по-своему. Два сервиса в одном проекте, использующие одни и те же технологии и общающиеся друг с другом через NATS, могли быть структурно совершенно разными. Один сервис — с чёткими слоями, портами и адаптерами. Соседний — плоская структура, вся логика в одном файле, репозиторий и бизнес-логика перемешаны. Оба работают. Оба написаны профессионалами. Но вместе они образуют систему, в которой нет никакой предсказуемости.
Когда разработчик уходит, его сервис превращается в чёрный ящик. Формально — это TypeScript и NATS, как у всех. Фактически — у него своя философия, свои соглашения, свой способ думать о коде. Новый разработчик заходит внутрь и обнаруживает чужой мир. Он либо тратит неделю на погружение, либо начинает переписывать по своим правилам. Второе случалось чаще.
Каждый проект держался на экспертах. Перевести разработчика между проектами было болезненно — фактически онбординг приходилось проходить заново, причём иногда для каждого сервиса отдельно. Уход ключевого архитектора всегда был риском: часть решений и правил существовала только в его голове.
Мы поняли, что проблема не в людях и не в технологиях — проблема в отсутствии ядра. Каждый проект изобретал своё. И мы решили создать капсулу — фреймворк, который станет общим ядром для всех наших проектов.
От принципов к фреймворку
Первым делом нам нужно было извлечь опыт из голов и формализовать его. Последовала серия созвонов, на которых мы составили список ключевых архитектурных принципов. Список отсортирован от самых крупных к более мелким:
Микросервисная архитектура. Нам нужна капсула именно для распределённых систем, а для нас это в первую очередь микросервисы.
Архитектура, управляемая событиями (EDA). По нашему опыту, коммуникация через генерацию и обработку событий — наиболее гибкий подход. Нет жёсткой связи между сервисами, нет цепочек синхронных вызовов. Это накладывает дополнительную сложность, но гибкость для нас важнее.
Гексагональная архитектура сервисов. Мы часто меняем технологии в рамках проекта, поэтому важно отделить бизнес-логику от периферии. В гексагональной архитектуре (она же «Порты и Адаптеры») в центре находится бизнес-логика, а всё необходимое скрыто за интерфейсами — портами. В момент выполнения бизнес-логика получает конкретную реализацию порта — адаптер.
Инверсия зависимостей через инъекцию. Придерживаясь гексагональной архитектуры, нам нужно реализовать инверсию зависимостей. Мы выбрали инъекцию через DI-контейнер.
Логи и телеметрия. Распределённые системы сложно отслеживать. Стандартом выбран OpenTelemetry — и важно, что трассировка охватывает не только синхронные вызовы, но и асинхронные события, чтобы вся цепочка обработки оказывалась в рамках одной трассировки.
Минимализм и следование техрадару. Все технологии капсулы должны быть в техрадаре компании на уровне
Adopt. Никаких случайных зависимостей.
Из этих принципов вытекли два ключевых технических следствия: кодогенерация (фреймворк диктует жёсткую структуру, значит её нужно генерировать, а не писать руками) и архитектурный альбом (структура должна автоматически визуализироваться в C4-диаграммы).
Выбор технологий из техрадара был быстрым: TypeScript, NATS, OpenTelemetry. Этого оказалось достаточно.
Как это работает: полный цикл создания сервиса
Лучший способ понять фреймворк — пройти через него целиком. Возьмём конкретный пример: сервис управления заказами (OrderService), который принимает заказы и публикует событие при их создании.
Шаг 1. Описываем сервис в схеме
Всё начинается с файла service.schema.json. Разработчик описывает методы сервиса и события, которые он генерирует. Никакого кода — только структура данных.
{ "name": "OrderService", "description": "Сервис управления заказами", "methods": { "CreateOrder": { "action": "CreateOrder", "description": "Создание нового заказа", "request": { "type": "object", "properties": { "credentials": { "type": "object", "properties": { "user_id": { "type": "string" } }, "required": ["user_id"] }, "data": { "type": "object", "properties": { "product_id": { "type": "string", "description": "Идентификатор продукта" }, "quantity": { "type": "number", "description": "Количество единиц" } }, "required": ["product_id", "quantity"] } }, "required": ["credentials", "data"] }, "response": { "type": "object", "properties": { "result": { "type": "object", "properties": { "order_id": { "type": "string" } }, "required": ["order_id"] } }, "required": ["result"] } } }, "events": { "list": { "OrderCreated": { "action": "OrderService.event.order-created", "description": "Событие создания нового заказа", "options": { "stream": true }, "event": { "type": "object", "properties": { "order_id": { "type": "string" }, "user_id": { "type": "string" }, "product_id": { "type": "string" }, "quantity": { "type": "number" } }, "required": ["order_id", "user_id", "product_id", "quantity"] } } }, "streamOptions": { "prefix": "stream", "actions": [{ "action": ">", "storage": "file", "messageTTL": 86400 }] } } }
Обратите внимание на несколько деталей. Каждое поле request и response — это полноценная JSON Schema с типами, валидацией и описаниями. Событие OrderCreated помечено флагом stream: true, что означает — оно будет храниться в JetStream и доступно для обработки в течение заданного времени (здесь messageTTL: 86400 — одни сутки).
Шаг 2. Кодогенерация
После описания схемы запускается генератор. На выходе появляются файлы, которые никогда не редактируются вручную:
interfaces.ts — TypeScript-типы для всех методов и событий. Генерируется автоматически на основе JSON-схем, редактировать нельзя:
// Типы запросов и ответов методов export interface CreateOrderRequest { credentials: { user_id: string; space_id: string; }; data: { product_id: string; quantity: number; }; } export interface CreateOrderResponse { result: { order_id: string; }; } // Тип события export interface OrderCreatedEvent { order_id: string; user_id: string; space_id: string; product_id: string; quantity: number; } // Эмиттер для использования ВНУТРИ сервиса (публикация) export type EmitterOrder = { OrderCreated: (data: OrderCreatedEvent, uniqId?: string) => void; }; // Эмиттер для использования КЛИЕНТАМИ (подписка) export type EmitterOrderExternal = { OrderCreated: EventStreamHandler<OrderCreatedEvent>; };
index.ts — типизированный клиент сервиса для других сервисов. Любой сервис, который хочет вызвать метод OrderService, импортирует этот клиент и получает полную типизацию — без необходимости знать о деталях NATS.
Итого кодогенератор создаёт следующую структуру директорий — и она одинакова для всех сервисов в системе:
service/ ├── domain/ │ ├── aggregates/ # Бизнес-сущности │ └── ports/ # Интерфейсы репозиториев ├── infra/ │ ├── repositories/ # Реализации репозиториев │ └── components/ # Инфраструктурные компоненты ├── methods/ # Классы методов (наследуют BaseMethod) ├── processing/ # Обработчики событий от других сервисов ├── service.schema.json # Схема — единственный источник истины ├── interfaces.ts # Автогенерация — не редактировать ├── index.ts # Клиент сервиса — автогенерация ├── service.ts # DI-контейнер и запуск └── start.ts # Точка входа
Любой разработчик, открывший незнакомый сервис, сразу знает, что и где. Это не конвенция из документа — это физическое ограничение, которое капсула накладывает на всех.
methods/CreateOrder/index.ts — скелет метода:
// Сгенерированный скелет — разработчик дописывает только handler export class CreateOrder extends BaseMethod<EmitterOrder> { static settings = serviceSchema.methods.CreateOrder; constructor() { super(); } async handler(request: CreateOrderRequest): Promise<CreateOrderResponse> { // TODO: implement throw new Error('Not implemented'); } }
Шаг 3. Реализуем метод
Разработчик дописывает только бизнес-логику. Никаких деталей NATS, никакой маршрутизации, никакой сериализации — только то, что относится к предметной области:
export class CreateOrder extends BaseMethod<EmitterOrder> { static settings = serviceSchema.methods.CreateOrder; constructor( @inject(TYPES.OrderRepo) private readonly repo: IOrderRepo ) { super(); } async handler({ credentials, data }: CreateOrderRequest): Promise<CreateOrderResponse> { const order = this.repo.create({ user_id: credentials.user_id, space_id: credentials.space_id, product_id: data.product_id, quantity: data.quantity, }); await this.repo.persist(order, credentials); this.emitter.OrderCreated({ order_id: order.getId(), user_id: credentials.user_id, space_id: credentials.space_id, product_id: data.product_id, quantity: data.quantity, }); return { result: { order_id: order.getId() }, }; } }
Метод получает репозиторий через DI (@inject), работает с агрегатом, публикует событие и возвращает ответ. Больше ничего. Фреймворк берёт на себя: подписку на NATS-тему, десериализацию входящего сообщения, валидацию по JSON Schema, сериализацию ответа, отправку трассировки в OpenTelemetry.
Шаг 4. Публикуем событие
Когда сервис хочет опубликовать событие, он вызывает типизированный эмиттер — this.emitter.OrderCreated(...). Фреймворк знает о событии из схемы и публикует его в нужный NATS-стрим.
Никаких строк с названиями топиков, никаких JSON.stringify. Тип OrderCreatedEvent проверяется компилятором — если поле изменилось в схеме и была запущена кодогенерация, TypeScript сразу укажет на все места, которые нужно обновить.
Шаг 5. Подписываемся на события другого сервиса
Другой сервис (например, NotificationService) хочет реагировать на создание заказа. Для этого создаётся класс-обработчик в папке processing/:
export class OrderProcessing { constructor( @inject(TYPES.NotificationRepo) private readonly repo: INotificationRepo ) {} private async onOrderCreated(event: { data: OrderCreatedEvent; ack: () => void; nak: (ms: number) => void; }) { try { await this.repo.sendOrderConfirmation(event.data); event.ack(); // подтверждаем обработку } catch (error) { event.nak(5000); // повтор через 5 секунд } } public start(orderClient: OrderClient) { const listener = orderClient.getListener('NotificationService', { deliver: 'new' }); listener.on('OrderCreated', this.onOrderCreated.bind(this)); } }
Механизм ack/nak — это гарантия доставки JetStream. Если обработчик упал, не вызвав ack, брокер повторит доставку через указанное время. Это встроено в фреймворк и работает автоматически.
Шаг 6. Запускаем сервис
service.ts — точка сборки. Здесь настраивается DI-контейнер и запускается сервис:
export async function main(broker?: NatsConnection) { const brokerConnection = broker || (await connect({ servers: configs.application.natsHost, maxReconnectAttempts: -1, })); // Настраиваем DI container.bind<OrderRepo>(TYPES.OrderRepo, DependencyType.ADAPTER, OrderRepo, { init: true, // репозиторий установит соединение с БД при init() }); // Запускаем сервис const service = new Service<EmitterOrder>({ name: serviceSchema.name, brokerConnection, methods: [CreateOrder, GetOrder, ListOrders, CancelOrder], events: serviceSchema.events, gracefulShutdown: { additional: await container.initDependencies(), // инициализируем и сразу передаём для graceful shutdown }, }); await service.start(); return service; }
Вызов container.initDependencies() — обязательный шаг, если хотя бы один адаптер зарегистрирован с опцией init: true. Фреймворк вызывает метод init() у таких адаптеров в нужном порядке и возвращает массив инициализированных объектов — его удобно сразу передать в gracefulShutdown.additional, чтобы при остановке сервиса соединения закрылись корректно.
Массив methods — это все классы методов сервиса. Фреймворк сам подписывает каждый метод на соответствующую NATS-тему, исходя из поля action в схеме. Добавить новый метод — значит написать его класс и добавить в этот массив. Всё.
Ключевые возможности фреймворка
Пройдя по циклу создания сервиса, можно увидеть, как каждый принцип воплощается в конкретные возможности.
Request/Reply через NATS
Каждый метод сервиса — это NATS Request/Reply. Клиентский код вызывает метод как обычную асинхронную функцию:
const orderClient = serviceInstance.buildService(OrderClient); const result = await orderClient.CreateOrder({ credentials: { user_id, space_id }, data: { product_id: 'prod-42', quantity: 3 }, });
Под капотом это NATS-запрос с ожиданием ответа, таймаутом и автоматической десериализацией. С точки зрения вызывающего кода — обычный вызов TypeScript-функции с полной типизацией.
Pub/Sub с гарантией доставки через JetStream
События с флагом stream: true публикуются в NATS JetStream. Это означает:
Сообщения хранятся на брокере в течение
messageTTL.Подписчики могут получать события с любой точки: с момента подписки (
deliver: 'new'), с начала стрима (deliver: 'all') или с последнего необработанного (deliver: 'last').Гарантия exactly-once обработки через
ack/nak— даже если обработчик упал, событие будет доставлено повторно.
Без фреймворка настройка JetStream — это многострочный boilerplate. В капсуле всё это задаётся в схеме через streamOptions и работает «из коробки».
Параметр deliver при подписке — важная деталь, которая определяет поведение при запуске и перезапуске сервиса:
deliver: 'new'— обрабатывать только события, появившиеся после старта подписки. Подходит для большинства случаев: реакция на новые факты.deliver: 'all'— начать с самого первого события в стриме. Подходит для event sourcing: восстановление состояния при старте или при развёртывании нового сервиса, которому нужно «догнать» историю.
Батчевая обработка событий
Для высоконагруженных сценариев фреймворк поддерживает батчевый режим. Вместо обработки одного события за раз обработчик получает массив:
const batchListener = orderClient.getListener('NotificationService:Batch', { deliver: 'new', batch: true, maxPullRequestBatch: 50, // максимум событий в одном батче maxPullRequestExpires: 3000, // ждать не более 3 секунд до отправки неполного батча }); batchListener.on('OrderCreated', async (messages, meter) => { meter.start(); for (const message of messages) { try { await this.repo.sendOrderConfirmation(message.data); message.ack(); } catch { message.nak(5000); } } meter.end(); });
Это принципиально меняет производительность при обработке тысяч событий: вместо тысячи round-trip к базе данных можно сделать один батчевый INSERT.
Runtime-валидация по JSON Schema
Каждый метод может включить автоматическую валидацию входных и выходных данных:
"options": { "runTimeValidation": { "request": true, "response": true } }
Фреймворк компилирует JSON Schema в валидатор при старте и проверяет данные до вызова handler. Если данные не соответствуют схеме — клиент получит структурированную ошибку валидации, а handler не будет вызван.
Кеширование методов
Для методов с дорогостоящими запросами можно включить кеш прямо в схеме:
"options": { "cache": 5 }
Значение 5 означает кеширование на 5 секунд. При повторном вызове с теми же параметрами фреймворк вернёт кешированный ответ, не доходя до handler. Реализацию кеша разработчик передаёт при старте сервиса — это может быть Redis, in-memory или что угодно другое, реализующее интерфейс get/set.
Web-стримы для больших данных
NATS имеет лимит на размер сообщения — 1 МБ. Для передачи файлов или больших объёмов данных фреймворк поддерживает Web-стримы:
"options": { "useStream": { "request": true, "response": true } }
В этом режиме NATS используется только для балансировки нагрузки — чтобы найти нужный инстанс сервиса. Сами данные передаются по HTTP напрямую. Для вызывающего кода это абсолютно прозрачно — интерфейс остаётся тем же.
Встроенная телеметрия
Фреймворк автоматически инструментирует каждый метод и каждое событие для OpenTelemetry. Когда запрос проходит цепочку из нескольких сервисов — через синхронные вызовы и асинхронные события — всё это оказывается в рамках одной распределённой трассировки. Разработчику не нужно добавлять ничего вручную.
Пользователь делает запрос и получает ответ. Разработчик по трассировке этого же запроса видит не только вызовы между сервисами и запросы к базам данных, но и все порождённые в результате события — и то, как их обрабатывают другие сервисы.
Архитектурный альбом
Жёсткая структура сервисов открывает возможность, которой нет в большинстве проектов: архитектуру можно не описывать вручную — её можно читать автоматически.
Для этого в корне проекта живёт файл archland.json — архитектурный ландшафт системы. Он описывает:
Сервисы — список всех сервисов с путями к схемам и каталогам
Зависимости — что и через что подключает каждый сервис: репозитории, конфиги, клиенты других сервисов
Методы — что каждый метод вызывает и какие события генерирует
Подписки — на какие события подписан сервис, что делает обработчик и какие события генерирует в ответ
Фрагмент archland.json для двух связанных сервисов из нашего примера:
{ "format": "1.0", "name": "MySystem", "version": "1.0.0", "services": { "OrderService": { "describePath": "services/order/service.schema.json", "description": "Сервис управления заказами", "folderPath": "services/order/", "dependencies": { "TYPES.OrderRepo": { "shared": false, "className": "OrderRepo", "classPath": "./infra/repositories/OrderRepo/index.ts", "type": "dbms", "external": false, "dependencies": [] } }, "methods": { "CreateOrder": { "description": "Создание нового заказа", "trigger": ["OrderCreated"], "calls": [ { "dependencyKey": "TYPES.OrderRepo", "method": "persist" } ] } }, "subscriptions": [] }, "NotificationService": { "describePath": "services/notification/service.schema.json", "description": "Сервис уведомлений", "folderPath": "services/notification/", "dependencies": {}, "methods": {}, "subscriptions": [ { "from": "OrderService", "event": "OrderCreated", "dependencyKey": "TYPES.OrderProcessing", "calls": [ { "dependencyKey": "TYPES.NotificationRepo", "method": "sendOrderConfirmation" } ], "trigger": [] } ] } } }
Ключевые поля
Разберём поля, которые несут смысловую нагрузку для анализа архитектуры.
Каждая запись в dependencies — это одна инъекция в DI-контейнере:
type— категория зависимости:dbms(база данных),service(клиент другого сервиса),adapter(инфраструктурный компонент),constant(конфиг),api(внешний HTTP-сервис). Именно это поле определяет, как зависимость отображается на диаграмме: сервисы — прямоугольниками-контейнерами, базы данных — цилиндрами.external— флаг, указывающий, что зависимость находится за пределами системы: сторонние API, облачные сервисы. Такие зависимости рисуются за границей системы — за System Boundary.shared— флаг общей зависимости:true, если класс находится вне каталога текущего сервиса (например, вshared/). Позволяет визуально видеть, что несколько сервисов используют одну реализацию.dependencies— список ключей DI, от которых зависит сама эта зависимость. Благодаря этому компонентная диаграмма строится с правильной иерархией: видно, что репозиторий зависит от конфига с настройками подключения, а не просто «существует».
Каждый метод в methods описывает своё поведение в двух разрезах:
trigger— список событий, которые метод публикует черезthis.emitter.*. Именно здесь фиксируется, чтоCreateOrderпорождаетOrderCreated.calls— список вызовов зависимостей: какой метод у какой зависимости вызывается. На компонентной диаграмме это стрелка от метода к репозиторию или к клиенту другого сервиса.
Подписки в subscriptions замыкают картину:
fromиevent— откуда пришло событие и как оно называется.dependencyKey— какой класс-обработчик его принимает (ключ из DI).callsиtrigger— то же, что у методов: что обработчик вызывает и что публикует в ответ.
От данных к диаграммам
archland.json — это не диаграмма. Это данные, из которых диаграмму можно построить по любым правилам и в любом инструменте. Мы генерируем PlantUML и строим C4 на двух уровнях.
Контейнерная диаграмма — вид на всю систему сверху. Правила простые: каждый сервис — это Container, каждая внешняя база данных — ContainerDb. Синхронный вызов из calls одного сервиса к другому рисуется сплошной стрелкой (-->). Подписка в subscriptions рисуется пунктирной стрелкой (..>) от сервиса-источника к сервису-подписчику. В итоге диаграмма честно показывает: где система общается синхронно (и значит, связана), а где — через события (и значит, развязана).
Компонентная диаграмма строится для каждого сервиса отдельно. Методы и обработчики событий — это Component. Репозитории и внешние сервисы — ContainerDb_Ext и Container_Ext. Стрелки от методов к зависимостям берутся из calls, входящие события — из subscriptions, исходящие — из trigger. Получается полная карта внутренней жизни сервиса.
На диаграммах автоматически подсвечиваются проблемные места:
Синхронные вызовы между сервисами — жёлтым: они создают связанность, и при их росте система деградирует в распределённый монолит.
Петли синхронных вызовов — красным: потенциальный deadlock.
События без подписчиков — красными стрелками: кто-то публикует, но никто не слушает.
Такая детализация позволяет не только рисовать диаграммы, но и считать архитектурные метрики: количество синхронных связей между сервисами, глубину цепочек вызовов, количество «мёртвых» событий. Архитектор за несколько минут видит, что изменилось за последний месяц — достаточно сравнить два снимка archland.json.
Примеры готовых диаграмм — контекстной, контейнерной и компонентной — вы увидите в третьей части цикла. Там мы разберём, как AI-агент сам генерирует архитектурный альбом из archland.json: читает код сервисов, заполняет структуру и строит диаграммы по документированному контракту.
Раньше archland.json генерировался отдельным скриптом, который обходил файловую систему и разбирал код по жёстким паттернам. Это работало — но только пока структура сервисов не отклонялась от шаблона ни на шаг. Нестандартное именование зависимостей, необычный способ подписки на событие, репозиторий, вынесенный в infra/components/ вместо infra/repositories/ — всё это ломало скрипт.
Сегодня archland.json генерирует AI-агент. Он читает код сервисов и по документации формата заполняет структуру: понимает нестандартные случаи, умеет задавать уточняющие вопросы и объясняет, что именно он нашёл. Структура файла строго задокументирована — и это то, что нужно агенту: не угадывать формат, а работать по известному контракту.
О том, как именно агент работает с капсулами — в третьей части цикла.
Итого
Когда фреймворк заработал в реальных проектах, эффект проявился не сразу — но проявился конкретно. Новый разработчик, приходя на проект, больше не тратил первые дни на изучение местных соглашений: структура везде одинаковая, контракты читаются из схемы. Перевод разработчика между проектами перестал быть болезненным. Архитектурные расхождения между проектами — которые раньше накапливались незаметно и обнаруживались спустя месяцы — стали видны на диаграммах сразу.
Фреймворк — это не изолированная капсула. Он живёт внутри иерархии других капсул компании: техрадар определяет его технологический стек, а он сам определяет структуру всех проектов. Когда техрадар переводит технологию в статус «больше не используем» — это прямой сигнал к обновлению фреймворка, что автоматически затрагивает все проекты на нём.
Мы рассмотрели, как из набора принципов рождается конкретная капсула — со своей схемой, кодогенерацией, DI-контейнером, событийной архитектурой и архитектурным альбомом. Ни один из этих элементов не случаен: каждый появился из опыта предыдущих проектов и решает конкретную проблему, с которой мы сталкивались.
Это не универсальный инструмент. Другая команда, с другим контекстом и другим техрадаром, написала бы другой фреймворк. Именно поэтому мы называем его капсулой — это слепок нашего опыта, а не абстрактная методология.
И здесь важно сделать шаг назад и посмотреть шире. В этой части мы говорили о фреймворке для разработки — но методология капсул не ограничивается кодом. Капсула — это способ организовать любой повторяющийся процесс. Оценка проекта на входе, найм разработчика, онбординг нового члена команды — каждый из этих процессов тоже можно упаковать в капсулу. Зафиксировать опыт, который иначе живёт только в голове у конкретного человека, придать ему форму и сделать воспроизводимым.
При этом форма капсулы определяется природой самого процесса. Для разработки это фреймворк с кодогенерацией. Для оценки проекта — возможно, математическая модель с весовыми коэффициентами. Для онбординга — структурированный план с чеклистами и точками контроля. Форма разная, суть одна: опыт перестаёт зависеть от конкретного носителя.
Именно поэтому слово «капсула» точнее, чем «фреймворк» или «методология». Капсулы — это не про очередной технический инструмент. Это про то, как компания накапливает и передаёт опыт — в разработке, в управлении, в найме, в чём угодно.
Капсула = Экспертное знание + Минимализм + Готовый инструмент
Наш фреймворк — это одна из таких капсул. Экспертное знание: принципы распределённых систем, выработанные на нескольких проектах. Минимализм: только для распределенных систем на TypeScript, NATS и OpenTelemetry — ничего лишнего. Готовый инструмент: кодогенерация, DI-контейнер, архитектурный альбом — всё работает из коробки.
В третьей части я покажу, что происходит, когда к этой капсуле подключается AI-агент. Как он работает с фреймворком, что ему даёт структура капсулы и как это меняет сам процесс разработки.
