Onion, Clean и Hexagonal Architecture

В чём разница?
10 мин чтения

Общая задача трёх архитектур

Onion, Clean и Hexagonal Architecture решают одну проблему: защищают бизнес-логику от инфраструктуры. Под инфраструктурой здесь понимают базу данных, внешние API, обращения к другим сервисам и прочие технические детали. Правила проверки заказа не должны ломаться оттого, что команда сменила СУБД или брокер сообщений.
Все три подхода держатся на одном принципе.
Принцип инверсии зависимостей (Dependency Inversion Principle, DIP): модули верхнего уровня не должны зависеть от модулей нижнего уровня, и те и другие должны зависеть от абстракций. Абстракции не должны зависеть от деталей, детали должны зависеть от абстракций.
Разберём на примере оформления онлайн-заказа. На практике интерфейсы лежат в ядре, а реализации — в инфраструктурном слое. Ядро объявляет OrderRepository на своём языке: найти заказ, сохранить заказ — без единого слова про SQL. Класс PostgresOrderRepository в инфраструктуре реализует этот интерфейс, и при запуске приложение подставляет его на место интерфейса.
Инверсия зависимостей: HTTP-контроллер вызывает сценарий оформления заказа, а инфраструктурный репозиторий реализует интерфейс ядра
Поток выполнения (пунктир) проходит насквозь от контроллера до PostgresOrderRepository, а зависимости в коде (сплошные стрелки) направлены внутрь, к ядру.
Поэтому спор о том, какая из трёх архитектур «правильная», начинается с ложной предпосылки. Это близкие подходы с общей идеей и разными акцентами. Разберём каждый, начав с привычной схемы, от которой они все отталкиваются.

Hexagonal Architecture: фокус на границе приложения

Гексагональная архитектура, которую ещё называют «порты и адаптеры», показывает приложение как ядро с портами по краям.
Входные адаптеры принимают запросы извне: HTTP-контроллер, gRPC-сервер, Kafka-консьюмер. Каждый вызывает входной порт, то есть use case «оформить заказ» в логике приложения. С другой стороны, ядро объявляет исходящие порты OrderRepository и EventPublisher. Это интерфейсы, и лежат они в ядре. Исходящие адаптеры для PostgreSQL, Kafka или gRPC их реализуют.
Hexagonal Architecture: входные адаптеры вызывают входной порт, а исходящие адаптеры реализуют порты ядра
Все стрелки направлены к ядру: адаптеры зависят от портов, а ядро не знает ни об одном адаптере.
Главное, что даёт этот подход: инфраструктуру можно легко заменить, не меняя ядро. Событие о заказе сегодня уходит через Kafka, а завтра через gRPC-клиент. Для этого достаточно написать новый адаптер под тот же EventPublisher.
Чётких инструкций, как устроить само ядро, Hexagonal не даёт. Автор подхода Алистер Коберн оставил этот вопрос открытым.

Clean Architecture: фокус на сценариях использования

Clean Architecture Роберта Мартина заглядывает внутрь ядра и делит его на слои:
  • Entities содержат общие бизнес-правила. Заказ сам проверяет, можно ли его оформить.
  • Use Cases описывают конкретные сценарии приложения, например «оформить заказ».
  • Interface Adapters передают данные между ядром и внешними зависимостями. Ту же роль в Hexagonal Architecture играют адаптеры.
Сильная сторона Clean Architecture в том, что у Use Case (сценариев приложения) есть явное место в ядре. И каждый Use Case реализует один из сценариев приложения:
interface OrderRepository {
    Order findById(OrderId orderId);
    void save(Order order);
}

interface EventPublisher {
    void publish(OrderPlaced event);
}

public class PlaceOrderUseCase {
    private final OrderRepository orders;
    private final EventPublisher events;

    public PlaceOrderUseCase(OrderRepository orders, EventPublisher events) {
        this.orders = orders;
        this.events = events;
    }

    public void execute(OrderId orderId) {
        Order order = orders.findById(orderId);
        order.place(); // правила оформления проверяет сам заказ
        orders.save(order);
        events.publish(new OrderPlaced(order.id()));
    }
}
Use Case отвечает только за оркестрацию сценария. А решение, можно ли оформить заказ, принимает order.place(), поэтому сценарий остаётся тонким. Интерфейсы OrderRepository и EventPublisher объявлены рядом с ним. Фреймворки и база остаются во внешнем кольце, так же как адаптеры в Hexagonal лежат снаружи шестиугольника.
Clean Architecture: контроллер вызывает Use Case, тот обращается к Entity и исходящим портам
Поток выполнения проходит через кольцо с Use Cases: оно принимает вызов от контроллера, передаёт решение сущности и обращается наружу только через интерфейсы.

Onion Architecture: фокус на детализации ядра

Onion Architecture Джеффри Палермо детализирует ядро ещё сильнее. В центре лежит доменная модель, вокруг неё остальные кольца:
  • Domain Model: набор классов, которые отражают понятие предметной области: заказ, пользователь и так далее.
  • Domain Services: бизнес-логика, которая работает с несколькими доменными объектами и не принадлежит ни одному из них.
  • Application Services: по сути, Use Cases — полный аналог сценариев использования в Clean Architecture.
  • Инфраструктура снаружи: UI, база, интеграции, тесты. По сути адаптеры.
Onion Architecture: доменная модель в центре, вокруг доменные и прикладные сервисы, снаружи инфраструктура
Onion особенно ясно отвечает, какая часть бизнес-логики должна быть в ядре и где именно. Правила заказа живут в доменной модели, логика над несколькими доменными объектами уходит в доменные сервисы, а Application Services отвечают за сценарии приложения (Use Case). Отделить ядро от инфраструктуры умеют все три подхода. Но Onion детальнее всех раскладывает само ядро и выделяет в нём доменную модель.

Какая лучше, что выбрать?

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

Объединяем лучшее

Вместо выбора одной из трёх лучше взять от каждой лучшее: порты и адаптеры у Hexagonal, явные use cases у Clean, детализированное доменное ядро у Onion. Получается такая картина: адаптеры снаружи, порты на границе, сценарии приложения внутри, доменная модель в центре.
Объединённая архитектура: адаптеры снаружи, порты на границе, сценарии приложения внутри и доменное ядро в центре

DDD наполняет ядро смыслом

Все три архитектуры объясняют, как защитить ядро. Ключевой вопрос они оставляют открытым: как спроектировать саму доменную модель? И без ответа на этот вопрос мы, скорее всего, получим тот же DTO Order с геттерами и сеттерами, только в другой папке.
Ответ даёт Domain-Driven Design. DDD добавляет строительные блоки Entity, Value Object, Aggregate, Domain Event, Repository и общий язык, на котором разработчики и бизнес одинаково называют одни и те же вещи. Дополним ими схему:
DDD в объединённой архитектуре: агрегат Order содержит Entity и Value Object, порождает Domain Event и сохраняется через Repository
Заказ стал агрегатом, его оформление порождает Domain Event OrderPlaced, а Repository из DDD занял место исходящего порта.
В итоге теперь у каждого подхода своя роль. Hexagonal задаёт границы взаимодействия, Clean организует сценарии, Onion показывает устройство ядра, а DDD помогает это ядро спроектировать.

От схем к работающему сервису

Сложно? На самом деле, если двигаться последовательно с опытным ментором, можно во всём детально разобраться. На курсе «DDD и Clean Architecture» ты с нуля строишь сервис и последовательно применяешь эти практики вместо того, чтобы заучивать различия между подходами. Каждый модуль заканчивается заданием на кодирование: Value Object, Entity, Aggregate, use cases, адаптеры HTTP, gRPC и Kafka.
На выходе ты понимаешь термины, умеешь раскладывать ответственность по границам и получаешь структурированный сервис, который легче поддерживать и тестировать. Его архитектура понятна, проверяема и готова к развитию. Курс есть в трёх версиях: на C#, Java и Go.

А зачем это всё, особенно когда код пишет агент?

Явные границы помогают и людям, и агентам, которые пишут код. Агенту проще продолжать кодовую базу, когда по структуре видно, где use case, где доменное правило, где порт и где адаптер. Всегда понятно, куда поместить новый сценарий, а новая интеграция всегда уйдёт в адаптер.

Вывод

Onion, Clean и Hexagonal Architecture похожи не случайно: все три разворачивают зависимости от инфраструктуры к бизнес-логике. Различаются они тем, на какой вопрос отвечают подробнее. Поэтому вместо выбора одной схемы бери лучшее от каждой.
Мой канал в Telegram
Ветчинкин Кирилл
Backend, DDD, Clean Architecture и AI-агенты.
Делюсь опытом и примерами кода.
Архитектура Backend
приложений