Пять уровней погружения в сложную систему

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

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

AOA переносит этот способ из головы опытного разработчика в структуру самой системы. От проекта к проекту она сохраняет пять одинаковых опор: Domain, Action, Contract, Pipeline и Code. Каждая опора становится отдельным уровнем погружения, а вместе они задают последовательный путь от общей картины к конкретному поведению.

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

Документация ↗

Когда все уровни контекста смешаны

Что мешает увидеть общую картину, когда все детали доступны одновременно?

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

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

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

плоская кодовая база · новые факты без опоры
orders.pyBillingServiceapi.pycreate_orderreserve_stockOrderModelsend_mailutils.pydb.py
пять уровней · контекст строится последовательно
01Domainгде живёт ответственность
02Actionчто умеет система
03Contractчто операция требует и обещает
04Pipelineкак развивается сценарий
05Codeгде находится конкретное поведение
Полная документация ↗

Сначала — карта ответственности

Где живёт ответственность?

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

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

Что даёт карта доменов

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

4 × ActionStoreDomain

Отвечает за корзину, заказ и передачу в доставку

3 × ActionBillingDomain

Отвечает за платежи, счета и возвраты

3 × ActionMessagingDomain

Отвечает за коммуникации с клиентом и вебхуки

3 × ActionAnalyticsDomain

Отвечает за события, витрины и бизнес-отчётность

Полная документация ↗

Карта превращается в каталог возможностей

Что умеет система?

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

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

Что даёт каталог действий

На схеме выделен CreateOrderAction — действие, которое запускает создание заказа. Рядом показаны другие действия StoreDomain и связи между ними. Одна операция может опираться на другую, но зависимости направлены и не замыкаются в цикл. На этом уровне видны только имена действий и их связи; входные данные, результат, сценарий и код будут раскрыты дальше.

StoreDomainименованные исполняемые возможности
CreateOrderAction

Создать заказ: проверить, зарезервировать и оплатить

→ ChargePaymentAction

GetOrderAction

Получить актуальное состояние заказа

CancelOrderAction

Отменить заказ и запустить возврат

→ RefundPaymentAction

ShipOrderAction

Передать подтверждённый заказ в доставку

Полная документация ↗

Фиксируем границу операции

Что операция требует и обещает?

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

Contract делает границу Action типизированной и однозначной. Params описывает всё, что операция принимает, Result — всё, что она обязана вернуть. Вызывающий код зависит от этого обещания, а не от устройства Action внутри. Реализацию можно менять, пока вход, выход и наблюдаемое поведение сохраняют совместимость.

Что даёт контракт действия

У знакомого CreateOrderAction появляется полная сигнатура: слева CreateOrderParams, справа CreateOrderResult. Поля больше не существуют как абстрактные модели данных — они относятся к конкретной операции с понятным местом и назначением. Внутренность Action по-прежнему не требуется для его использования.

CreateOrderParams
customer_id: str
items: list[Item]
currency: str
CreateOrderAction

Оркестрировать создание заказа за одной объявленной границей

Params → Result
CreateOrderResult
order_id: str
payment_id: str
status: OrderStatus
Полная документация ↗

Разворачиваем операцию в сценарий

Как развивается сценарий?

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

Pipeline делает основной бизнес-сценарий линейным и видимым: проверить вход, зарезервировать товар, провести оплату, собрать результат. Это не означает, что в реальности нет ошибок, развилок и компенсаций; они получают явное место относительно основной линии. Каждый шаг тоже имеет локальный контракт: объявляет, что читает из Params, State и Context, и что добавляет в State для следующих этапов.

Что даёт сценарий операции

Домен дал сценарию место, Action — имя, Contract — начало и обещанный итог. Pipeline добавляет причинный порядок и внутренние границы. Шаг reserve_inventory выделен как отдельное изменение состояния: он получает уже проверенные данные и оставляет reservation_id для продолжения сценария.

regularvalidatevalidated_items
regularreserve_inventoryreservation_id
regularcharge_paymentpayment_id
summarycreate_resultOrderResult
Полная документация ↗

Только теперь — конкретное поведение

Где находится конкретное поведение?

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

Code становится пятым уровнем не потому, что детали не важны, а потому, что теперь они окружены смыслом. Уже известно, какому домену принадлежит поведение, какую возможность реализует Action, что обещает Contract и какое место занимает reserve_inventory в Pipeline. Реализация читается как локальный ответ на конкретную задачу, а не как вход в бесконечное расследование репозитория.

Что даёт изоляция поведения в отдельном шаге

Открывая reserve_inventory, мы заранее знаем его намерение, входы и место в сценарии. Принцип остаётся тем же: внешние эффекты должны проходить через объявленные Resource и публичные Action. Архитектурная грамматика делает отклонения видимыми и проверяемыми, не притворяясь, что один только текст описания уже физически запрещает любой обход.

store/actions/create_order.py
@regular_aspect("Reserve inventory")@result_string("reservation_id", required=True)@context_requires("user.tenant_id", "env.inventory_region")async def reserve_inventory_aspect(    self, params, state, box, connections, ctx):    inventory = box.resolve(InventoryResource)    reservation_id = await inventory.reserve(        tenant_id=ctx.get("user.tenant_id"),        region=ctx.get("env.inventory_region"),        items=state["validated_items"],    )    return {"reservation_id": reservation_id}
$ uv run python store/actions/create_order.py
вывод
Полная документация ↗

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

Пять уровней погружения в сложную систему · aoa.run