Domain-Driven Design (DDD) звучит как серебряная пуля. Когда мы начинаем проект на ASP.NET, идея четкого разделения на слои, изоляция бизнес-логики в домене и использование паттернов вроде Repository и Unit of Work кажется идеальной архитектурой.

Но есть один нюанс: магия DDD начинает испаряться ровно в тот момент, когда количество агрегатов (реестров) в проекте переваливает за 30. То, что было элегантным решением для CRM с 10 сущностями, превращается в бюрократический ад для ERP-системы или крупного маркетплейса.

В этой статье я разберу, почему классический DDD в ASP.NET (особенно в связке с Entity Framework Core) становится узким местом на масштабных проектах.

Проблема «Толстых» Агрегатов и Производительности

Один из главных постулатов DDD гласит: один запрос — один агрегат. Агрегат должен быть консистентным, и загружать его нужно целиком.

Принцип «Один запрос — один агрегат» (также известен как «одна транзакция на агрегат») — принцип предметно-ориентированного проектирования (Domain-Driven Design, DDD). Он предполагает, что запрос клиента влияет на один агрегат, и изменения в системе происходят через методы этого агрегата.

модель из этой статьи
модель из этой статьи

Реальность

Когда у вас 30+ реестров, у каждого из них есть глубокие графы вложенных объектов (Value Objects, дочерние сущности). Если следовать правилам строго:

  • Вы начинаете использовать Include и ThenInclude в Entity Framework Core, которые порождают монструозные SQL-запросы с десятками JOIN.

var orders = await context.Orders
    .Include(o => o.Customer)
        .ThenInclude(c => c.Address)
    .Include(o => o.Customer)
        .ThenInclude(c => c.Contacts)
    .Include(o => o.Items)
        .ThenInclude(i => i.Product)
            .ThenInclude(p => p.Category)
    .Include(o => o.Items)
        .ThenInclude(i => i.Discount)
    .ToListAsync();
SELECT 
    -- Orders
    [o].[Id], [o].[OrderNumber], [o].[OrderDate], [o].[CustomerId], [o].[Total],
    
    -- Customer (через первый Include)
    [c].[Id], [c].[Name], [c].[Email], [c].[Phone], [c].[CustomerId],
    
    -- Address (через ThenInclude c.Address)
    [a].[Id], [a].[Street], [a].[City], [a].[PostalCode], [a].[Country], [a].[AddressId],
    
    -- Contacts (через второй ThenInclude c.Contacts)
    [c1].[Id], [c1].[ContactType], [c1].[Value], [c1].[CustomerId],
    
    -- Items (через Include o.Items)
    [i].[Id], [i].[OrderId], [i].[ProductId], [i].[Quantity], [i].[Price],
    
    -- Product (через ThenInclude i.Product)
    [p].[Id], [p].[Name], [p].[Description], [p].[Price], [p].[CategoryId],
    
    -- Category (через ThenInclude p.Category)
    [cat].[Id], [cat].[Name], [cat].[Description], [cat].[ParentId],
    
    -- Discount (через второй ThenInclude i.Discount)
    [d].[Id], [d].[Code], [d].[Amount], [d].[Type], [d].[ItemId]

FROM [Orders] AS [o]

-- Customer (первый Include)
LEFT JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]

-- Address (ThenInclude c.Address)
LEFT JOIN [Addresses] AS [a] ON [c].[AddressId] = [a].[Id]

-- Contacts (ThenInclude c.Contacts)
LEFT JOIN [Contacts] AS [c1] ON [c].[Id] = [c1].[CustomerId]

-- Items (второй Include)
LEFT JOIN [OrderItems] AS [i] ON [o].[Id] = [i].[OrderId]

-- Product (ThenInclude i.Product)
LEFT JOIN [Products] AS [p] ON [i].[ProductId] = [p].[Id]

-- Category (ThenInclude p.Category)
LEFT JOIN [Categories] AS [cat] ON [p].[CategoryId] = [cat].[Id]

-- Discount (ThenInclude i.Discount)
LEFT JOIN [Discounts] AS [d] ON [i].[DiscountId] = [d].[Id]

ORDER BY 
    [o].[Id], 
    [c].[Id], 
    [a].[Id], 
    [c1].[Id], 
    [i].[Id], 
    [p].[Id], 
    [cat].[Id], 
    [d].[Id]
  • Чтобы обновить одно поле в заголовке заказа, вы вынуждены выгружать из БД 50 вложенных позиций (OrderItems), потому что они являются частью агрегата Order.

На 5-10 реестрах это еще терпимо. Когда реестров 40, а в каждом из них по 5-6 уровней вложенности, производительность падает катастрофически. Время отклика API растет, а разработчики начинают нарушать принципы DDD, добавляя специальные «читаемые модели» (Read Models) прямо в доменный слой, размывая границы.

Лавинообразный рост «Спецификаций» (Specifications)

Для инкапсуляции логики выборки в DDD часто используют паттерн Specification. Это удобно, когда нужно переиспользовать условия фильтрации.

паттерн Specification
паттерн Specification

Проблема

При 30+ реестрах папка Specifications превращается в свалку из сотен классов. Вы быстро осознаете, что:

  1. Спецификации сложно композировать. В реальных бизнес-задачах фильтры зависят от роли пользователя, статуса документа, временных зон и связанных данных. Попытки склеить спецификации через And и Or приводят к генерации неоптимального SQL.

  2. Утечка абстракций. Спецификации, написанные под EF Core, часто завязаны на IQueryable. Как только вам нужно будет вытащить данные через Dapper (для отчета) или через gRPC, спецификация становится бесполезной.

Разработка занимает больше времени на написание «красивых» спецификаций и репозиториев, чем на реализацию самой бизнес-логики.

Инфраструктурная сложность: Unit of Work и Scoped DbContext

В ASP.NET Core стандартный подход — Scoped DbContext. В малых проектах он отлично играет роль Unit of Work. Но в больших системах с 30+ реестрами появляются кросс-агрегатные транзакции.

Где боль?

DDD учит нас, что одна транзакция = изменение одного агрегата. Но бизнесу плевать на DDD. Бизнесу нужно, чтобы при создании «Заказа» одновременно создавался «Платеж», резервировался «Товар» на складе и создавалась «Задача» в колл-центре.

Если у вас 30+ реестров, вы неизбежно столкнетесь с ситуациями, где один Use Case должен менять 5+ агрегатов.

  • Использовать одну транзакцию БД (классический Unit of Work) — значит нарушить DDD и создать гигантские God-сервисы.

  • Использовать распределенные транзакции (Saga, Outbox) — значит в 10 раз увеличить сложность кода.

В проектах с 30+ реестрами часто оказывается, что 70% кода — это не бизнес-логика, а инфраструктурный «клей» для синхронизации агрегатов между собой.

Навигация по коду: «Ад» от количества файлов

DDD подразумевает глубокую структуру папок по слоям (API, Application, Domain, Infrastructure) и по модулям (Features).

При 30+ реестрах это выливается в проект с более чем 1000+ файлов. Простой кейс «Добавить поле в сущность» требует изменений в 10-15 файлах:

  1. Доменная сущность.

  2. Конфигурация маппинга (Fluent API).

  3. Миграция.

  4. Команда (Command).

  5. Валидатор команды (FluentValidation).

  6. Обработчик (Handler).

  7. DTO.

  8. Маппер (AutoMapper/Manual).

  9. Возможно, спецификация.

  10. Обновление тестов (Unit/Integration).

Это не просто «неудобно». Это снижает скорость итераций. В больших коммерческих проектах время — деньги, и жесткая приверженность DDD начинает восприниматься как наследие, требующее рефакторинга.

Заключение

DDD в ASP.NET — это мощный инструмент для моделирования сложной бизнес-логики, но он не масштабируется линейно. Если у вас в проекте более 30 реестров, и вы пытаетесь для каждого из них построить классическую луковую архитектуру (Onion Architecture) с репозиториями, спецификациями и богатой доменной моделью, вас ждет:

  • Падение производительности из-за тяжелых агрегатов.

  • Медленная разработка новых фичей (из-за необходимости трогать 10+ слоев).

  • Проблемы с поддержкой и онбордингом новых разработчиков.

Спасением в больших проектах является гибридный подход: использование принципов DDD только в зонах высокой сложности (ядре) и применение более прагматичных подходов (CQRS, Vertical Slices, Feature-Sliced Design) для периферийных реестров и запросов.

Помните: архитектура должна помогать бизнесу, а не быть фетишем. Если ради поддержки принципов DDD вы начинаете тратить в 2 раза больше времени на реализацию простого справочника — возможно, вы стали заложником своих же абстракций.

Комментарии (45)


  1. Arm79
    26.03.2026 04:11

    Поделитесь, пожалуйста, примером агрегата с 5 (или больше) уровнями вложенности. Просто для расширения кругозора


    1. Dhwtj
      26.03.2026 04:11

      один Use Case должен менять 5+ агрегатов, а не сложный агрегат


      1. Arm79
        26.03.2026 04:11

        В тексте статьи четко написано про 5-6 уровней вложенности


        1. Dhwtj
          26.03.2026 04:11

          И то и другое написано.

          В общем, примеры интересно послушать в любом случае


    1. Dhwtj
      26.03.2026 04:11

      CRM пример придумал - смена владельца аккаунта:

      1. Account - переводится другому менеджеру

      2. Contact - все контакты в аккаунте переходят менеджеру

      3. Deal - все сделки в аккаунте переходят менеджеру

      4. ActivityLog - история активностей перепривязывается

      5. TeamQuota - квоты старого и нового менеджера пересчитываются (выручка, количество аккаунтов)

      Здесь действительно нужна полная консистентность. Иначе:

      • Менеджер видит аккаунт, но контакты не его

      • Квота неправильная

      • Отчеты врут


      1. Arm79
        26.03.2026 04:11

        Тут не один агрегат, а много. И налицо событийная модель.

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


        1. Dhwtj
          26.03.2026 04:11

          Вы хотите пример с глубокой вложенностью агрегатов? Ну это бред


  1. Dhwtj
    26.03.2026 04:11

    Хорошо сформулированы проблемы.

    Меры борьбы:

    • EF - write sise, dapper + view (вместо спецификаций) read side

    • Файлы объединяем - Command + Handler + Validator, Query + Handler + DTO, сущность отдельно

    • один Use Case должен менять 5+ агрегатов => доменные события или outbox или Saga, но вероятно в полной мере бизнесу это не нужно (я не встречал)


  1. lexnext1
    26.03.2026 04:11

    Несколько соображений.

    1. Лучше не тащить orm, особенно в ddd. Да, ef core крут и даст фору многим, но все-таки каждый инструмент под свою задачу. Решился на ddd, тогда не надо экономить на sql.

    2. Попробовать перестать крутиться вокруг агрегатов, как самых что ни на есть самых самостоятельных участников системы и дать шанс сущностям (вот этим несамостоятельным) - размыть границы. Это даст гибкости.

    3. Забыть про dto. 1001-е лихо сформированное дтошка добьет проект и ddd тут не спасет. Dto в больших проектах нет. Есть request, response. И тут появляются хэндлеры - да, но не mediatr.

    4. Пересмотреть репозитарии и их имплементацию, отстуствие orm дает крутые бонусы и отличную управляемость с производительностью, если опять же слепо не следовать канонам и не тащить агрегаты на каждый запрос. Как следствие появляется некоторый промежуточный инструмент компромисса: response. Но с ним надо быть осторожно. Здесь больше про оптимизацию.

    5. Не забывать про ValueObjects. Бизнес скажет спасибо.

    Профит.


    1. Gromilo
      26.03.2026 04:11

      1. Решился на ddd, тогда не надо экономить на sql.

      А как собирать агрегаты и потом сохраняться изменения? Мне казалось, что DDD без ORM не живут.


      1. lexnext1
        26.03.2026 04:11

        Ну, если подразумевать агрегат по Эвансу, то рецепт его сборки из бд следующий: оптимизированная CTE, желательно Dapper (удобен он, не отнять), и дальше в репозитрии сборка агргегата через CollectionsMarshal и спанов с использование конструкторов или стат методов агрегата. Производительность по сравнению с ef core ощутимая. При реализациии сразу видишь и бд (запрос) и агрегат - уже понимаешь варианты оптимизаци.

        А при создании агрегата без сохранения (формат ин мемори) или перед сохранением, тут чаще всего вообще не нужен персистенс слой (читать бд и конкретные поля таблиц) - все в аппликейшн через те же, а может и другие более подходящие, чем используемые в репозитариях, конструкторы или стат методы агрегата.

        В обоих случаях мы получаем валидный агргегат соответствующий всем учтенным в нем бизнес правилам.


        1. Gromilo
          26.03.2026 04:11

          с использование конструкторов или стат методов агрегата.

          Нужно будет защититься, чтобы кто угодно не мог собрать что угодно. Я бы не хотел валидироваться при каждой сборке объекта из БД.

          Как собрать - я понял, а как сохранять без ченж трекинга? Я вижу 2 варианта: сохранять всё или знать снаружи, что нам нужно сохранить. Может знаешь как проще?

          Я просто довольно долго работал с даппером (мой пр в даппер) и сохранение было больным местом. Нужно знать что сохраняешь. А если нужно обновить списки связки, то туши свет, кто во что горазд. В итоге, либо всё сохраняешь, либо используешь методы, которые обновляют заданные поля в БД.


          1. lexnext1
            26.03.2026 04:11

            1)

            >>> а как сохранять без ченж трекинга? 

            Без чейндж трекинга, конечно, можно сохранять, но все-таки у нас ddd и исключать такую фичу - не стоит. Поэтому репозитарий слой обычно про чейндж трекер ничего не знает, он работает с бд и ограничено с агрегатами (см. п.2) - перегружать логикой не стоит.

            Каждый агрегат унаследован от RootAggregate в котором есть: IReadOnlyCollection<IDomainEvent> Events { get; } void ClearDomainEvents(); void AddDomainEvent(IEvent @event); - это в доменном слое. А дальше уже Application слой. Подписка идет через специальные EventHandler (дженерик обычно, где типы TAggregate и TDomainEvent). Обрабтываются события в апликейшн на уровне UnitOfWork в конкретном хендлере.

            2)

            >>> Нужно будет защититься

            Однозначно - да. У нас же не го - мы себе можем позволить базовый комфорт. Пример: связка internal + дружественные сборки - помогут предовратить неправильную эксплуатацию аггрегата.


            1. Gromilo
              26.03.2026 04:11

              Спасибо за ответ, про такое решение не подумал


            1. monco83
              26.03.2026 04:11

              Ну и как сохранять то, без ChangeTracker? Хочу услышать ответ. Написать свой ChangeTracker вместо ORM?


      1. monco83
        26.03.2026 04:11

        >Мне казалось, что DDD без ORM не живут.

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

        И тут корень противоречия. DDD на "философском уровне" про "Tackling Complexity in the Heart of Software" и про полное абстрагирование от способов хранения. А на практике - "если у вас нет ORM, то нет DDD". И ведь действительно очень сложно реализовать стандартные DDD-паттерны проектирования без ORM, если у вас не документоориентированная база данных.


        1. AlexViolin
          26.03.2026 04:11

          Странная тема обсуждения "DDD с/без ORM". DDD (логика домена) находится в logic layer. ORM используется в persistence layer. Логика домена, когда в неё приходят данные, полученные из бд, вообще не имеет никакого представления о том, что эти данные были получены при помощи ORM или какими-то другими механизмами.


          1. monco83
            26.03.2026 04:11

            Так очевидно же. Как выглядит типичный код с применением DDD?

            var request = repository.GetRequest(requestId);
            request.Approve(userId);
            repository.SaveChanges();

            Ну и как вы в данном случае реализуете репозиторий без ORM? ChangeTracker в рукопашку забабахаете?

            Ну и сами "доменные модели". Я застал ещё время, когда вся эта доменка представляла собой набор свойств с {get; set}и конструктором по умолчанию. Потому что EF (без Core) ничего сложнее мапить не умел. Сейчас EF многому научился, но уши персистентности из моделей всё равно там и тут торчат. Вот и получается, что проектирование _моделей_, самого сердца DDD, прочно связано с возможностями ORM.


    1. Dhwtj
      26.03.2026 04:11

      Если размывать ради гибкости SQL-запросов на чтение - да, CQRS read side может обходить агрегаты. Если размывать на записи - это путь обратно к transaction script

      Value objects для making illegal states unrepresentable внутри домена - конечно!


      1. Gromilo
        26.03.2026 04:11

         CQRS read side может обходить агрегаты

        Получается, что наш домен - это наша БД?

        Я к тому, что читать тоже нужно уметь и логика чтения и логика в агрегатах может будет дублироваться.

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


        1. iprs
          26.03.2026 04:11

          А еще вместо табличек (реляционной БД) может быть вообще что угодно. Хоть текстовые файлы определенного формата. И ничего при смене технологии хранения менять не придется, кроме реализации "репозитория". И да, в серьезных проектах - иногда приходится менять систему хранения на более подходящую. И в итоге часть агрегатов живет в postgresql, часть в elastic, а часть вообще в другой системе с доступом по rest api. А в слое домена все однотипное.


          1. Gromilo
            26.03.2026 04:11

            Это я понимаю.

            Из-за этого мы и не пользуемся преимуществами реляционных БД. Фактически создаём себе ограничения, а потом героически их преодолеваем :D

            Особенно смешно, когда кроме одной БД на проекте ничего и нет :)


            1. iprs
              26.03.2026 04:11

              Я за свою жизнь ни разу не встречал проекта, которому бы хватило одной БД для всего. Абсолютно на всех местах работы системы вроде как и декларировались самодостаточными, но по факту зависели от других. И в итоге постоянно решались проблемы синхронизации данных между ними. Обычно костылями. Так что я давно уже не верю, что можно "транзакцией в БД" решить все проблемы. А уж в нынешнее время, когда микросервисы пихают всюду, и где это оправдано, и просто чтобы быть "как все" - можно смело забывать эти преимущества и привыкать работать по новому)


              1. Gromilo
                26.03.2026 04:11

                Я со всем согласен. Просто не понимаю, зачем усложнять себе жизнь при работе в одной БД на ровном месте. На интеграции с другими сервисами хватит развлечений :)


                1. iprs
                  26.03.2026 04:11

                  Это скорее был пример, что не стоит делать архитектуру вида "спагетти". Даже в одной БД имеет смысл разделение на модули. Транзакции, затрагивающие 50 таблиц тоже ведь так себе решение.


                  1. Gromilo
                    26.03.2026 04:11

                    Соглы, архитектура важна Я успешно делаю модульные монолиты и выношу модули в отдельные сервисы по необходимости, когда понятно, что пора.


                    1. iprs
                      26.03.2026 04:11

                      Аналогично, пришел со временем к такой же архитектуре


              1. monco83
                26.03.2026 04:11

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


        1. Dhwtj
          26.03.2026 04:11

          Зачем вообще агрегат для чтения? Вызываешь репозиторий для этой сущности и читаешь что надо


  1. Arm79
    26.03.2026 04:11

    Что видится мне:

    1. Что-то не то с уровнями вложенности. 5-6 уровней - возможно, агрегат стоит пересмотреть и разбить его на более мелкие.

    2. Если есть необходимость менять сразу несколько агрегатов, то стандартный подход - доменные события. Не настолько это затратно по разработке, чтобы бизнес встал на дыбы. Тем более речь идёт о достаточно тяжелом приложении с аж 30+ агрегатами. Вложишься сейчас, сэкономишь время разработчика в будущем.

    3. Свалка из сотен классов спецификаций может быть сгруппирована поагрегатно

    4. Неоптимизированные запросы в случае спецификаций - известная проблема, которая решается упрощением агрегатов.

    5. Если вас беспокоит невозможность переиспользовать спецификации в dapper, рассмотрите отказ от dapper в пользу любого другого orm с поддержкой iqueryable. Мне не совсем понятно, зачем при наличии EF вам второй orm, но если что то легковесное нужно - linq2db есть

    6. "DDD подразумевает глубокую структуру папок по слоям" - вообще нет. Это clean architecture. Но вообще разделение на слои, наоборот, помогает ориентироваться в коде

    7. "«Добавить поле в сущность» требует изменений в 10-15 файлах" - в большом коммерческом продукте это хорошо и правильно. Если у вас возникает очень часто такая необходимость, рассмотрите возможность работы с сущностями с динамическим набором атрибутов


  1. iprs
    26.03.2026 04:11

    Бизнесу нужно, чтобы при создании «Заказа» одновременно создавался «Платеж», резервировался «Товар» на складе и создавалась «Задача» в колл-центре.

    Постоянно слышу от противников/сомневающихся в DDD подобные примеры, что он, дескать, не работает в реальном мире. Да, наверное, где-то и есть такой бизнес, где абсолютно все реализуется в одной системе и можно решать задачи одной транзакцией.

    Только вот в реальном мире на складе одна система, платежами заведует другая, колл-центром третья, половина из них - куплены как готовые решения (и часто без штатных механизмов интеграции). Ну, и как в таких условиях будете решать эту задачу? Поможет тут DDD или нет?


    1. Gromilo
      26.03.2026 04:11

      Если декомпозиция сделано хоть как-нибудь правильно, то нам удобно в своём сервисе работать в транзакции, а с остальными сервисами "интегрироваться". Под интеграцию закладывается отдельное время разработки, т.к. вообще хз что может вылезти. А потом ещё и тестить тяжко и стрелять может долго.


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

      Для большинства моих задач для интеграции с другими сервисами мне хватает TransactionOutbox, позволяя надёжно выполнить изменения в другой системе после фиксации в моей (положить сообщение в очередь, дёрнуть внешний метод и т.п.).


      1. iprs
        26.03.2026 04:11

        Я имел в виду, возражения в духе "бизнесу нужно чтобы одновременно" - через DDD не решаются или решаются неудобно. Но по факту, оказывается что "бизнес" задержка в "одновременности" в пару секунд - вполне устраивает. И в итоге DDD вполне работает и облегчает разработку. Агрегат сохраняется вместе с событиями (тот самый outbox) в одной транзакции в свои таблички, а после этого события расползаются по связанным агрегатам и системам, производя свои изменения неявно для первого агрегата. Система стает слабо связанной и более устойчивой к сбоям.


        1. Gromilo
          26.03.2026 04:11

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

          Что будет внутри обработчика вообще без разницы: DDD агрегаты, вызов хранимки или призыв Ктулху.

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


          1. iprs
            26.03.2026 04:11

            Ну, в общем то да. DDD просто снижает когнитивную сложность.


            1. Gromilo
              26.03.2026 04:11

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

              Даже подкаст писал на эту тему.


  1. AlexViolin
    26.03.2026 04:11

    один Use Case должен менять 5+ агрегатов.

    Наверное это ключевой вопрос, который возникает при использовании DDD. Юз кейс работает с ключевым агрегатом, но при его обработке могут меняться и другие агрегаты. Следуя логике 1 агрегат = 1 транзакция в этом юз кейс в бд сохраняется только ключевой агрегат. По остальным агрегатам формируются доменные события с описаниям изменений агрегатов и эти события сохраняются таблицу событий Outbox. Здесь и возникает вопрос - кто будет просматривать эти данные в Outbox и вызывать юз кейсы для изменения каждого агрегата. Здесь логически продолжаю цепочку - у каждого юз кейс только один агрегат изменяется в транзакции. Значит для изменения каждого агрегата должен быть отдельный юз кейс. Если архитектура микросервисная у которой 1 микросервис=1 агрегат, то можно дёргать нужный микросервис для каждого изменённого агрегата. А если архитектура монолитная - то как?


    1. Gromilo
      26.03.2026 04:11

      то будет просматривать эти данные в Outbox и вызывать юз кейсы для изменения каждого агрегата.

      А если архитектура монолитная - то как?

      А в чём проблема? На аутбокс событие есть подписчик, который дёргает юскейс.

      А как сделан подписчик не так уж важно: может напрямую из БД читает, может через очередь из дибезиума получает.

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


      1. AlexViolin
        26.03.2026 04:11

        А если агрегаты находятся в разных бд?


        1. Gromilo
          26.03.2026 04:11

          Тогда городим. Выбора нет: либо распределённая транзакция, либо согласованность в конечном итоге.


    1. monco83
      26.03.2026 04:11

      Вот у меня есть сценарий - принятие пользователем приглашения о присоединении к системе.

      Пользователь проходит по ссылке-приглашению, в результате запись из таблички invitations удаляется, а запись в табличку users добавляется. Кто тут кому root aggregate и как через DDD должны быть организованы события, чтобы всё это дело не рассыпалось? Как это сделать без DDD я и так знаю.


      1. totsamiynixon
        26.03.2026 04:11

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

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

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

        В итоге у вас 2 бумажки - приглашение и копия карточки вашей учётной записи.


  1. S1908
    26.03.2026 04:11

    Что за агрегаты? У меня тут в апи бд с 300+ таблицами и все прекрасно работает в ddd. Просто надо не загружать сущности все сразу, ef умеет делить запросы, в ef можно ef sql писать!


  1. 55ki11
    26.03.2026 04:11

    Скорее всего вам надо было просто по другому определить aggregate root или выделить новые аггрегаты.

    Вообще DDD не учит: "одна транзакция - один агрегат", это ваша вольная интерпретация, не сбивайте молодежь, они и так писать не научились.

    А молодежь - все идем читать Эванса и Вернона)


  1. monco83
    26.03.2026 04:11

    Подпишусь под многим. Правда, тут не в ASP.NET и не в EF дело. И даже не в DDD, а в IT-евангелизме как таковом.

    Все озвученные вами проблемы - они и на других платформах и с другими инструментами будут проявляться при внедрении хардкорного тру-DDD подхода. А вот если без хардкора, а просто взять без фанатизма предлагаемый DDD способ _переиспользования_кода_ (через модели), то и с DDD можно жить.

    Вообще, я вообще очень люблю книгу Эванса, но больше в soft-части, там где речь идёт о важности дистилляции понятий и моделей. К тактическим паттернам немало вопросов, но в книге для меня они не главные.