5 ключевых признаков, что приложение соответствует чистой архитектуре

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

Чистая архитектура — это не модный термин и не академическая абстракция. Это набор принципов, помогающих строить системы, которые легко развивать, тестировать и поддерживать годами.
Но как понять, соответствует ли ваша система этим принципам?

В этой статье разберем 5 конкретных признаков, что ваше приложение действительно построено по принципам чистой архитектуры. Проверь себя!
Чистая архитектура (Clean Architecture) — это архитектурный стиль, предложенный Робертом Мартином (Uncle Bob), целью которого является создание независимой, легко тестируемой, масштабируемой и сопровождаемой системы.

В её основе лежит разделение ответственности и направленная зависимость: всё зависит от бизнес-логики, но бизнес-логика — ни от чего.
1. Инфраструктура зависит об бизнес-логики, а не наоборот
Всё, что связано с реализацией бизнес-правил (например, расчёт скидок, начисление бонусов, логика маршрутизации заказов) должно быть независимо от фреймворков, баз данных, веб-серверов и UI.

Это значит, что бизнес-правила можно протестировать в отрыве от внешней среды — без поднятия базы, веб-приложения или зависимостей.

Если ваш основной код зависит от REST-контроллеров, Entity Framework, ORMs, UI-фреймворков — вы не в чистой архитектуре.

Пример нарушения:
[HttpPost("create")]
public IActionResult CreateOrder(CreateOrderRequest request)
{
    if (string.IsNullOrWhiteSpace(request.CustomerName))
        return BadRequest("Customer name required");

    var total = request.Items.Sum(i => i.Price * i.Quantity);

    if (total > 10000)
        return BadRequest("Total too high");

    _orderRepository.Save(request);
    return Ok();
}
Почему это плохо:
  • Такой код невозможно протестировать отдельно от HTTP-инфраструктуры
  • Логика расползается, копируется, не переиспользуется
  • При изменении бизнес-правил надо лезть в контроллеры

Как исправить:
  • Выделите Use Case (Application Service) — бизнес-логика должна быть вне контроллеров
  • Контроллер должен только делегировать вызов и переводить результат в HTTP-ответ
public interface ICreateOrderUseCase
{
    Task<Guid> Execute(CreateOrderDto dto);
}

public class CreateOrderUseCase : ICreateOrderUseCase
{
    private readonly IOrderRepository _repo;

    public CreateOrderUseCase(IOrderRepository repo)
    {
        _repo = repo;
    }

    public async Task<Guid> Execute(CreateOrderDto dto)
    {
        var order = new Order(dto.ProductId, dto.Quantity);
        await _repo.AddAsync(order);
        return order.Id;
    }
}
[HttpPost("create")]
public IActionResult CreateOrder(CreateOrderRequest request)
{
    var result = _createOrderUseCase.Execute(request);
    return result.IsSuccess ? Ok() : BadRequest(result.Error);
}
2. Внешние зависимости подключаются через интерфейсы и находятся на периферии
Базы данных, веб-интерфейсы, брокеры сообщений — это детали реализации.
В чистой архитектуре они не управляют логикой, а подключаются снаружи через интерфейсы, реализуемые на уровне инфраструктуры.

Ключ: направление зависимостей идёт от внешнего к внутреннему.
Приложение не зависит от технологий — технологии зависят от приложения.
Dependency Inversion Principle (DIP) — принципа из SOLID, согласно которому модули верхнего уровня не должны зависеть от деталей. В чистой архитектуре это означает, что интерфейсы определяются внутри (в слое Use Cases или Domain), а реализации — снаружи, на периферии (в инфраструктуре).

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

Как показано на схеме выше, инфраструктура зависит от Core, а не наоборот.
3. Модули слабо связаны между собой
Каждый модуль или компонент отвечает за свой кусок ответственности и не знает о реализации других. Связь происходит через абстракции: команды, события, интерфейсы.

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

  • Если меняем HTTP на gRPC - изменения каснутся только входящего адаптера.
  • Если меняем сценарий приложения - измениться только конкретный Use Case.
  • Если меняем Posrgress на MySQL - изменения каснутся только исходящего адаптера.
Common Closure Principle (Принцип общего закрытия) гласит:
Классы, которые меняются по одной и той же причине, должны быть сгруппированы в один модуль.

Иными словами:
  • Модуль (или пакет) должен иметь одну и ту же причину для изменений.
  • Если у нескольких классов единая причина измениться — они должны быть рядом.
  • Всё, что изменяется вместе — живёт вместе.

На схеме выше, у каждого модуля - своя зона ответственности. И он будет меняться только по причине.
4. Потоки данных и управляющая логика хорошо читаемы
При чтении кода UseCase (или Application Service) вы должны без труда понять:

  • Что инициировало процесс?
  • Какие шаги происходят?
  • Какие правила применяются?
  • Какие действия выполняются (сохранение, отправка уведомлений и т.д.)?

Чистая архитектура делает такие процессы явными и линейными, без скрытой магии, колбэков или глубоких зависимостей.
Это упрощает как отладку, так и онбординг новых участников команды.
5. Приложение легко покрывается тестами, особенно unit-тестами
Если вы можете писать юнит-тесты, не поднимая БД, не загружая UI и не конфигурируя DI-контейнер — это хороший признак.

Бизнес-логика тестируется быстро, без моков на 20 строк.

Если же тесты требуют настройки половины приложения и постоянно падают из-за изменений в инфраструктуре — это нарушает принципы чистой архитектуры.

Вот пример. В нем мы выделили адаптер для HTTP Handler, а логику приложения реализовали в UseCase:
public interface ICreateOrderUseCase
{
    Task<Guid> Execute(CreateOrderDto dto);
}

public class CreateOrderUseCase : ICreateOrderUseCase
{
    private readonly IOrderRepository _repo;

    public CreateOrderUseCase(IOrderRepository repo)
    {
        _repo = repo;
    }

    public async Task<Guid> Execute(CreateOrderDto dto)
    {
        var order = new Order(dto.ProductId, dto.Quantity);
        await _repo.AddAsync(order);
        return order.Id;
    }
}
[HttpPost("create")]
public IActionResult CreateOrder(CreateOrderRequest request)
{
    var result = _createOrderUseCase.Execute(request);
    return result.IsSuccess ? Ok() : BadRequest(result.Error);
}
public class CreateOrderUseCaseTests
{
    [Fact]
    public async Task Test_Order_Creation()
    {
        // Arrange
        var repoMock = new Mock<IOrderRepository>();
        var useCase = new CreateOrderUseCase(repoMock.Object);
        var dto = new CreateOrderDto { ProductId = Guid.NewGuid(), Quantity = 3 };

        // Act
        var orderId = await useCase.Execute(dto);

        // Assert
        repoMock.Verify(r => r.AddAsync(It.Is<Order>(o =>
            o.ProductId == dto.ProductId &&
            o.Quantity == dto.Quantity
        )), Times.Once);

        Assert.NotEqual(Guid.Empty, orderId);
    }
}
Что в итоге:
  • Юнит-тест без зависимостей (без ASP.NET и базы).
  • Проверяется бизнес-логика, а не инфраструктура.
  • Быстро, надёжно, просто.
Есть ли ещё нюансы?
Да, конечно.

Приведённые признаки — это база. Но в реальности придётся учитывать и другие важные аспекты:
  • управление зависимостями и инверсия потока управления
  • правильная работа с транзакциями
  • паттерны слоёв и модулей: Application Service, Gateway, Orchestrator и т.д.
  • границы Агрегатов и модулей
  • нюансы интеграции между сервисами в распределённой архитектуре
  • устойчивость к изменениям предметной области

Если вы хотите научиться выстраивать архитектуру, которая выдерживает рост команды и сложности, —
приглашаю на курс по Чистой архитектуре и DDD.

Там мы разберём не только фундамент, но и реальные кейсы, инструменты и практические решения.