Модель C4
Модель C4: четыре уровня схем от контекста до кода, что считать контейнером, сколько элементов влезает в картинку, чем рисовать и как схема не устаревает. Пример — маркетплейс.
На доске одна схема системы, и на ней всё сразу: сервисы, классы внутри одного из них, сетевые зоны, база, к которой ведут восемь стрелок без подписей. Заказчик спрашивает, где тут оплата, разработчики спорят, что значит пунктир, а через месяц половина коробок уже переименована. Такая схема отвечает на все вопросы одновременно и потому ни на один.
Модель C4 — договорённость, как разрезать эту картинку на несколько, у каждой из которых один вопрос и одна аудитория. Примеры ниже — на сквозном кейсе сайта, маркетплейсе.
Четыре уровня — четыре увеличения
Бизнес-заказчику нужно понять, с кем система связана, разработчику — куда положить код. Один рисунок на оба вопроса не отвечает: либо в нём нет деталей для второго, либо первый в них тонет. C4 разводит вопросы по уровням, как карта: страна, город, улица, дом.
Каждый уровень — увеличение одного элемента с предыдущего: маркетплейс целиком, потом Order Service внутри него, потом обработчик внутри сервиса. Тонированные два уровня рисуют всегда, остальные — по необходимости.
По-английски уровни называются Context, Container, Component, Code, и четвёрка в названии — число этих слов, а не номер уровня. У автора модели, Саймона Брауна (c4model.com), уровни так и называются: Level 1, Level 2. Сокращения C1–C4 прижились в командах, и здесь они тоже используются, но «C4» без уточнения — вся модель, а не уровень кода.
Про слово «контейнер» сразу: это не Docker. В C4 контейнер — всё, что запускается отдельно и внутри чего исполняется код или лежат данные; Docker-контейнер — лишь один из способов такое запустить.
В design doc обязательны первые два уровня: контекст объясняет, что строим, контейнеры — как. Компоненты рисуют для одного-двух сложных сервисов, код — почти никогда.
Что считать контейнером
Нарисовать контейнеры мешает не нотация, а граница: где кончается контейнер и начинается компонент. Правило одно — контейнер запускается и падает отдельно от остальных. Если у него свой процесс, своё развёртывание и с соседями он общается по сети — это контейнер. Если он живёт внутри процесса другого элемента и общается с соседями вызовом метода — компонент.
Отсюда контейнерами оказываются не только сервисы, базы и брокеры. Браузерное приложение на React и мобильное приложение — контейнеры: они исполняются на устройстве пользователя отдельно от бэкенда и ходят к нему по HTTP. Ночной пересчёт остатков, собранный из того же кода, что и Order Service, но запущенный отдельным процессом по расписанию, — контейнер: у него свой запуск и своё падение. Бессерверная функция, объектное хранилище, кэш — тоже контейнеры.
Не контейнер: модуль внутри модульного монолита (тот же процесс), библиотека, класс. Сайдкар вроде Envoy — формально отдельный процесс, но бизнес-логики он не несёт, поэтому на C2 его не рисуют: его место — диаграмма развёртывания. Три реплики одного пода в Kubernetes — по-прежнему один контейнер на C2: число экземпляров тоже относится к развёртыванию, а не к устройству.
Уровень 1: контекст
Первый вопрос про любую систему: что она делает и с кем связана. На него отвечает контекстная диаграмма, и её читают все — от продакт-менеджера до разработчика в первый день. Поэтому на ней три вида элементов и ни одной технологии: люди (роли, а не должности), сама система одной коробкой и внешние системы — те, которыми вы не управляете. Стрелка идёт от вызывающего к вызываемому, на стрелке — зачем связь нужна, а не по какому протоколу.
Внешняя система — это не обязательно чужая компания. Сервис соседней команды, к которому вы обращаетесь как к чёрному ящику, — тоже внешняя система для вашей: критерий — управляете ли вы им, а не где он стоит.
Три роли, система одной коробкой, пять внешних систем — и на каждой стрелке написано, зачем она: «заказы, оплата», «чеки», «выплаты продавцам». Ни одного протокола и ни одного сервиса внутри: это картинка для тех, кто спрашивает «что оно делает».
Что здесь было бы ошибкой. Добавить Kafka или разбить маркетплейс на сервисы — это ответ на вопрос «как», которого на этом уровне никто не задавал. Подписать стрелку «REST/JSON» вместо «заказы, оплата» — то же самое. И обратная ошибка: контекст на двадцать внешних систем. Если интеграций действительно столько, их группируют по назначению — «платёжные шлюзы» одной коробкой, как здесь, а не четыре банка-эквайера по отдельности.
Уровень 2: контейнеры
Команде нужно знать, что она разворачивает, что с чем общается и по какому каналу — без этого не спланировать ни выкат, ни поведение при отказе. Диаграмма контейнеров увеличивает коробку «система» с контекста и показывает её начинку. У каждого контейнера — имя из домена и технология («Order Service — Spring Boot» у Java-команды, «Order Service — Go» у Go-команды), у базы и очереди — своя фигура, чтобы хранилища отличались от сервисов с первого взгляда, у стрелки — что передаётся и как («создать заказ, REST»). Люди и внешние системы остаются с контекста, но теперь стрелки от них ведут к конкретным контейнерам.
Про сплошную и пунктирную стрелку: договорённость «сплошная — синхронно, пунктирная — асинхронно» распространена, но это не правило модели. Сама C4 нотационно-независима: Браун не задаёт ни форм, ни цветов, только требует легенду. Поэтому легенда обязательна на каждой диаграмме — без неё пунктир читают как «планируется» или «необязательная связь».
Граница системы обведена, внутри три ряда: вход (Customer API), доменные сервисы, хранилища. Стрелки к платёжному шлюзу и логистике уходят к внешним системам с контекста. Пунктир от Order Service к Kafka и дальше к Inventory Service — единственная асинхронная связь, и легенда обязана это сказать.
Что здесь было бы ошибкой. Коробка «Backend» вместо четырёх сервисов: с ней нельзя ответить, что откажет вместе с оплатой. Одна общая база на всех, когда на деле у каждого сервиса своя: схема обещает связность, которой нет. Стрелки без подписей: читатель не отличит «читает остатки» от «резервирует остатки», а это разные требования к отказоустойчивости. И контроллеры с репозиториями на этой же картинке — это уже следующий уровень.
Уровень 3: компоненты
Новый человек в сервисе спрашивает не «что с чем общается», а «куда положить код и через что пройдёт запрос». Диаграмма компонентов увеличивает один контейнер и показывает его внутренние части и путь запроса сквозь них. Рисуют её не для всех сервисов, а для одного-двух, где есть архитектурная сложность, и держат в README этого сервиса — там её найдёт тот, кому она нужна.
Компонент — не класс, а осмысленная единица: контроллер, сценарий, обработчик, порт к хранилищу. В терминах Use Case Pattern это ровно те блоки, из которых сервис собирается.
Путь одного запроса сверху вниз: контроллер принимает и валидирует, сценарий называет бизнес-шаг, обработчик ведёт транзакцию, порты выводят наружу. Технологии не повторяются — они известны с уровня контейнеров; здесь важна структура и то, где проходит граница между веб-слоем, сценарием и доступом к данным.
Ошибкой здесь будет схема на все сорок классов сервиса — это уже уровень кода — и компоненты для каждого сервиса подряд: CRUD из контроллера и репозитория такой картинки не заслуживает, она устареет раньше, чем её прочитают.
Уровень 4: код
Самый глубокий уровень — классы и интерфейсы одного компонента. Руками его не рисуют: класс переименовывают чаще, чем открывают документацию, и такая схема врёт уже через спринт. Если картинка нужна, чтобы объяснить хитрый узел на ревью, её генерируют из исходников — IDE и PlantUML строят диаграмму классов по коду — и не хранят. Поэтому в design doc этого уровня нет, и это не пробел.
Сколько элементов влезает
Рано или поздно диаграмма контейнеров перестаёт читаться: сорок коробок, стрелки пересекаются, подписи наезжают друг на друга. Порог читаемости — примерно двадцать элементов на картинке; дальше глаз не удерживает связи. Лечат не шрифтом, а разрезанием.
Первый способ — подняться выше контекста. Для нескольких систем, которые общаются между собой, в модели есть диаграмма ландшафта (System Landscape): каждая система одной коробкой, между ними связи; контекст каждой рисуется отдельно.
Второй — несколько C2 вместо одной, по подсистемам или по командам: «контейнеры заказов», «контейнеры каталога», а соседние подсистемы на каждой — одной коробкой без начинки. Читателю из команды каталога внутренности оплаты и не нужны.
Третий — убрать с C2 то, что на ней не про устройство. Мониторинг, логирование, service mesh, балансировщик: каждый сервис связан с ними, и такие стрелки только шумят. Их место — диаграмма развёртывания, а на C2 достаточно одной строки в легенде: «все сервисы шлют метрики в Prometheus».
Глубже: две диаграммы сверх уровней: сценарий и развёртываниерасширенное
Четыре уровня отвечают на вопрос «из чего система состоит». Два вопроса они не закрывают: в каком порядке всё происходит и на чём это стоит. Для них в модели есть две дополнительные диаграммы — не уровни, а другой взгляд на те же элементы.
Динамическая диаграмма (Dynamic) берёт элементы уже нарисованной C2 или C3 и нумерует стрелки по шагам одного сценария. Статичная C2 говорит, кто с кем вообще связан; динамическая — что за чем идёт и где сценарий ждёт ответа.
На C2 эти же связи были, но не было порядка. Здесь видно, что шаг 3 синхронный — не ответит Payment Service, заказ не сохранится и покупатель получит ошибку, — а шаги 5–6 асинхронные: между сохранением заказа и снятием остатка есть окно, в которое последнюю единицу товара успеет купить кто-то ещё. Это вопрос к дизайну, и без нумерации его не увидеть.
Диаграмма развёртывания (Deployment) раскладывает контейнеры по узлам: регионы, зоны доступности, серверы, сколько экземпляров каждого. По четырём уровням не ответить, что случится при отказе зоны, — там про железо ни слова.
Те же Customer API и Order Service, что на C2, но теперь видно, что их по два в каждой зоне, а база на запись одна — primary в зоне A. Отказ зоны A — не «половина мощности», а потеря записи до переключения на реплику; и каждая запись из зоны B платит задержкой за путь через границу зоны. Оба факта на C2 не видны.
Обе рисуют по необходимости: динамическую — когда сценарий длиннее трёх шагов или в нём есть ожидание внешнего ответа, развёртывание — когда в дизайне есть обязательства по доступности.
Глубже: чем рисовать и кто обновляетрасширенное
Схема, нарисованная в графическом редакторе и вставленная в вики картинкой, устаревает к третьему ревью: её меняет один человек, никто не видит разницы между версиями, и через полгода на ней нет двух новых сервисов. Поэтому диаграммы C4 держат текстом в git рядом с design doc — тогда правка схемы попадает в тот же запрос на слияние, что и код, и ревьюер видит diff.
C4-PlantUML — библиотека макросов для PlantUML: Person, System_Ext, Container, ContainerDb, Rel, System_Boundary. Текстовый файл, рендер в CI или плагином IDE, самый низкий порог входа. Эти имена встречаются в большинстве командных схем, но это соглашение инструмента, а не буква модели: спорить на ревью «в C4 положено писать System_Ext» не о чем. Слабое место: каждая диаграмма — отдельный файл, и сервис, переименованный на C2, на C1 и C3 останется под старым именем, пока кто-то не заметит.
Structurizr — инструмент самого Брауна, и он устроен иначе: описывается одна модель — элементы и связи, — а каждая диаграмма объявляется как вид на неё. Переименовали контейнер в модели — он переименован на всех видах; связь, которой нет в модели, на диаграмму не попадёт. Есть бесплатный Structurizr Lite для запуска на своей машине; цена — свой язык описания и ещё один сервис, который кто-то сопровождает.
Mermaid понимает C4-синтаксис и рендерится прямо в README на GitHub и GitLab — удобно для диаграммы компонентов в репозитории сервиса, но раскладку (сколько фигур и границ в ряду) задают руками, и для C2 всей системы это утомительно. Excalidraw и draw.io — для обсуждения у доски: быстро, но это картинка, а не модель, источником правды ей не быть.
Кто обновляет: автор изменения, в том же запросе на слияние, где добавлен контейнер, интеграция или поменялся протокол. Проверяется это пунктом чек-листа ревью, а не памятью. Если схема и код разошлись, врёт схема — это повод для ревью, а не для доверия картинке.
Коротко
- Один вопрос — одна картинка: контекст — «что и с кем», контейнеры — «из чего и по каким каналам», компоненты — «как устроен один сервис». Смешали — не читается ни одно.
- Контейнер — то, что запускается и падает отдельно: сервис, база, очередь, браузерное и мобильное приложение, задача по расписанию. Модуль, библиотека, сайдкар — нет.
- В design doc обязательны контекст и контейнеры; компоненты — для одного-двух сложных сервисов в их README; код руками не рисуют.
- Больше двадцати элементов — резать: ландшафт систем сверху, несколько C2 по подсистемам, инфраструктура — на развёртывание.
- Порядок шагов и точки ожидания видны только на динамической диаграмме, отказ зоны и число экземпляров — только на развёртывании.
- Схема живёт текстом в git рядом с кодом; обновляет автор изменения в том же запросе на слияние, проверяет чек-лист ревью. C4-PlantUML для старта, Structurizr — когда нужна одна модель на все виды.
- Легенда обязательна: модель не задаёт ни форм, ни стрелок; имена вроде
System_Extи пунктир за асинхронность — соглашения инструмента и команды.
Что почитать дальше
- Как оформить и защитить дизайн — где в design doc живут контекст и контейнеры и как их ревьюят.
- ADR — диаграмма показывает структуру, ADR — почему она такая; работают в паре.
- Структурные паттерны микросервисов — шлюз, BFF, сайдкар: то, что появляется на C2 и на развёртывании.
- Что делает архитектор — чья обязанность вести карту системы, которую C4 и рисует.