Модель C4

Модель C4: четыре уровня схем от контекста до кода, что считать контейнером, сколько элементов влезает в картинку, чем рисовать и как схема не устаревает. Пример — маркетплейс.

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

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

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

Обязательно

Четыре уровня — четыре увеличения

Бизнес-заказчику нужно понять, с кем система связана, разработчику — куда положить код. Один рисунок на оба вопроса не отвечает: либо в нём нет деталей для второго, либо первый в них тонет. C4 разводит вопросы по уровням, как карта: страна, город, улица, дом.

1 · Контекст читают: бизнес, менеджеры, новичок в команде система как чёрный ящик, её пользователи и внешние системы 2 · Контейнеры читают: архитекторы, разработчики сервисы, базы, очереди, клиенты и связи между ними 3 · Компоненты читают: команда этого сервиса внутри одного контейнера: контроллер, сценарий, порты 4 · Код руками не рисуют — генерируют из кода классы и интерфейсы одного компонента Маркетплейс → Order Service → CreateOrderHandler → Order

Каждый уровень — увеличение одного элемента с предыдущего: маркетплейс целиком, потом 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: контекст

Первый вопрос про любую систему: что она делает и с кем связана. На него отвечает контекстная диаграмма, и её читают все — от продакт-менеджера до разработчика в первый день. Поэтому на ней три вида элементов и ни одной технологии: люди (роли, а не должности), сама система одной коробкой и внешние системы — те, которыми вы не управляете. Стрелка идёт от вызывающего к вызываемому, на стрелке — зачем связь нужна, а не по какому протоколу.

Внешняя система — это не обязательно чужая компания. Сервис соседней команды, к которому вы обращаетесь как к чёрному ящику, — тоже внешняя система для вашей: критерий — управляете ли вы им, а не где он стоит.

Контекст маркетплейса: покупатель, продавец и оператор работают с системой; система обращается к платёжным шлюзам, логистике, фискальному оператору, SMS и e-mail и банку

Три роли, система одной коробкой, пять внешних систем — и на каждой стрелке написано, зачем она: «заказы, оплата», «чеки», «выплаты продавцам». Ни одного протокола и ни одного сервиса внутри: это картинка для тех, кто спрашивает «что оно делает».

Что здесь было бы ошибкой. Добавить Kafka или разбить маркетплейс на сервисы — это ответ на вопрос «как», которого на этом уровне никто не задавал. Подписать стрелку «REST/JSON» вместо «заказы, оплата» — то же самое. И обратная ошибка: контекст на двадцать внешних систем. Если интеграций действительно столько, их группируют по назначению — «платёжные шлюзы» одной коробкой, как здесь, а не четыре банка-эквайера по отдельности.

Уровень 2: контейнеры

Команде нужно знать, что она разворачивает, что с чем общается и по какому каналу — без этого не спланировать ни выкат, ни поведение при отказе. Диаграмма контейнеров увеличивает коробку «система» с контекста и показывает её начинку. У каждого контейнера — имя из домена и технология («Order Service — Spring Boot» у Java-команды, «Order Service — Go» у Go-команды), у базы и очереди — своя фигура, чтобы хранилища отличались от сервисов с первого взгляда, у стрелки — что передаётся и как («создать заказ, REST»). Люди и внешние системы остаются с контекста, но теперь стрелки от них ведут к конкретным контейнерам.

Про сплошную и пунктирную стрелку: договорённость «сплошная — синхронно, пунктирная — асинхронно» распространена, но это не правило модели. Сама C4 нотационно-независима: Браун не задаёт ни форм, ни цветов, только требует легенду. Поэтому легенда обязательна на каждой диаграмме — без неё пунктир читают как «планируется» или «необязательная связь».

Контейнеры маркетплейса: покупатель ходит в Customer API по HTTPS; API зовёт Catalog Service и Order Service по REST; Order Service пишет в PostgreSQL, синхронно зовёт Payment Service и логистику, публикует OrderCreated в Kafka, откуда его читает Inventory Service; Catalog Service ищет в Elasticsearch; Payment Service ходит во внешний платёжный шлюз

Граница системы обведена, внутри три ряда: вход (Customer API), доменные сервисы, хранилища. Стрелки к платёжному шлюзу и логистике уходят к внешним системам с контекста. Пунктир от Order Service к Kafka и дальше к Inventory Service — единственная асинхронная связь, и легенда обязана это сказать.

Что здесь было бы ошибкой. Коробка «Backend» вместо четырёх сервисов: с ней нельзя ответить, что откажет вместе с оплатой. Одна общая база на всех, когда на деле у каждого сервиса своя: схема обещает связность, которой нет. Стрелки без подписей: читатель не отличит «читает остатки» от «резервирует остатки», а это разные требования к отказоустойчивости. И контроллеры с репозиториями на этой же картинке — это уже следующий уровень.

Уровень 3: компоненты

Новый человек в сервисе спрашивает не «что с чем общается», а «куда положить код и через что пройдёт запрос». Диаграмма компонентов увеличивает один контейнер и показывает его внутренние части и путь запроса сквозь них. Рисуют её не для всех сервисов, а для одного-двух, где есть архитектурная сложность, и держат в README этого сервиса — там её найдёт тот, кому она нужна.

Компонент — не класс, а осмысленная единица: контроллер, сценарий, обработчик, порт к хранилищу. В терминах Use Case Pattern это ровно те блоки, из которых сервис собирается.

Компоненты Order Service: запрос POST /orders приходит из Customer API в OrderController, идёт в CreateOrderUseCase и CreateOrderHandler; обработчик пишет через OrderRepository в PostgreSQL, зовёт Payment Service по REST и через OrderEventPublisher публикует OrderCreated в Kafka

Путь одного запроса сверху вниз: контроллер принимает и валидирует, сценарий называет бизнес-шаг, обработчик ведёт транзакцию, порты выводят наружу. Технологии не повторяются — они известны с уровня контейнеров; здесь важна структура и то, где проходит граница между веб-слоем, сценарием и доступом к данным.

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

Уровень 4: код

Самый глубокий уровень — классы и интерфейсы одного компонента. Руками его не рисуют: класс переименовывают чаще, чем открывают документацию, и такая схема врёт уже через спринт. Если картинка нужна, чтобы объяснить хитрый узел на ревью, её генерируют из исходников — IDE и PlantUML строят диаграмму классов по коду — и не хранят. Поэтому в design doc этого уровня нет, и это не пробел.

Сколько элементов влезает

Рано или поздно диаграмма контейнеров перестаёт читаться: сорок коробок, стрелки пересекаются, подписи наезжают друг на друга. Порог читаемости — примерно двадцать элементов на картинке; дальше глаз не удерживает связи. Лечат не шрифтом, а разрезанием.

Первый способ — подняться выше контекста. Для нескольких систем, которые общаются между собой, в модели есть диаграмма ландшафта (System Landscape): каждая система одной коробкой, между ними связи; контекст каждой рисуется отдельно.

Второй — несколько C2 вместо одной, по подсистемам или по командам: «контейнеры заказов», «контейнеры каталога», а соседние подсистемы на каждой — одной коробкой без начинки. Читателю из команды каталога внутренности оплаты и не нужны.

Третий — убрать с C2 то, что на ней не про устройство. Мониторинг, логирование, service mesh, балансировщик: каждый сервис связан с ними, и такие стрелки только шумят. Их место — диаграмма развёртывания, а на C2 достаточно одной строки в легенде: «все сервисы шлют метрики в Prometheus».

Дополнительно: при первом чтении можно пропустить

Глубже: две диаграммы сверх уровней: сценарий и развёртываниерасширенное

Четыре уровня отвечают на вопрос «из чего система состоит». Два вопроса они не закрывают: в каком порядке всё происходит и на чём это стоит. Для них в модели есть две дополнительные диаграммы — не уровни, а другой взгляд на те же элементы.

Динамическая диаграмма (Dynamic) берёт элементы уже нарисованной C2 или C3 и нумерует стрелки по шагам одного сценария. Статичная C2 говорит, кто с кем вообще связан; динамическая — что за чем идёт и где сценарий ждёт ответа.

Оформление заказа по шагам: 1 покупатель шлёт POST /orders в Customer API, 2 API зовёт Order Service, 3 Order Service синхронно ждёт холд денег от Payment Service, 4 сохраняет заказ в PostgreSQL, 5 публикует OrderCreated в Kafka, 6 Inventory Service снимает остаток асинхронно

На C2 эти же связи были, но не было порядка. Здесь видно, что шаг 3 синхронный — не ответит Payment Service, заказ не сохранится и покупатель получит ошибку, — а шаги 5–6 асинхронные: между сохранением заказа и снятием остатка есть окно, в которое последнюю единицу товара успеет купить кто-то ещё. Это вопрос к дизайну, и без нумерации его не увидеть.

Диаграмма развёртывания (Deployment) раскладывает контейнеры по узлам: регионы, зоны доступности, серверы, сколько экземпляров каждого. По четырём уровням не ответить, что случится при отказе зоны, — там про железо ни слова.

Развёртывание маркетплейса: балансировщик направляет трафик в две зоны доступности, в каждой по два экземпляра Customer API и Order Service; primary PostgreSQL стоит в зоне A, реплика в зоне B; Order Service из зоны B пишет в primary через границу зоны

Те же 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 и пунктир за асинхронность — соглашения инструмента и команды.

Что почитать дальше