Help us improve
Share bugs, ideas, or general feedback.
From dex-skill-ddd
DDD — ловушки aggregate, entity, value object. Активируется при DDD, domain driven, aggregate, value object, domain event, bounded context, anemic model, domain service, specification, invariant, ubiquitous language, persisted state
How this skill is triggered — by the user, by Claude, or both
Slash command
/dex-skill-ddd:dddThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Плохо: `public Order Order { get; set; }` — навигация между агрегатами
Share bugs, ideas, or general feedback.
Плохо: public Order Order { get; set; } — навигация между агрегатами
Правильно: public int OrderId { get; private set; } — только ID
Почему: EF lazy-load тянет граф объектов (N+1), код меняет чужой Aggregate в обход Root, невозможно разнести по микросервисам
Плохо: order.Items.Add(new OrderItem(...)) — прямой доступ к коллекции
Правильно: order.AddItem(productId, quantity, price) — метод на Root
Почему: инварианты Aggregate не проверяются, бизнес-правила (лимит товаров, проверка статуса) обходятся
Плохо: Handler меняет Order + Inventory + Customer → один SaveChanges()
Правильно: Handler меняет один Aggregate, связь через Domain Events / Outbox
Почему: блокировки в БД, при росте нагрузки — deadlocks. Невозможно масштабировать, нарушает bounded context
Плохо: Order содержит Items, Payments, ShippingHistory, AuditLog — всё в одном Aggregate
Правильно: Order (Items), Payment (отдельный Aggregate), Shipment (отдельный Aggregate)
Почему: загрузка Order тянет всю историю, lock на Order блокирует оплату и доставку. Правило: если две сущности не обязаны быть консистентны в одной транзакции — разные Aggregate
Плохо: добавляешь в Entity / Owned-Type новое поле «на всякий случай» (нет читателя в коде), потому что «удобно посчитать здесь» (выводится из соседнего поля или из уже сохранённой структуры), или «понадобится в следующем спринте». Каждое такое поле уезжает в миграцию и в EF-конфигурацию
Правильно: критерий — есть ли читатель сейчас. Валидный потребитель: SQL-фильтр / сортировка / агрегация / индекс (WHERE, ORDER BY, GROUP BY) или Domain-логика, которой поле нужно как состояние для инварианта. Невалидный: показ пользователю поштучно (response одного объекта) — это считается в маппинге / проекции. Перед добавлением — grep будущих читателей: нет ни одного → не добавляй. Поле под другим именем и с другой семантикой относительно производной — добавляй, но имя честно отражает потребителя
Почему: persisted-поле без поискового сценария = миграция + схема + риск рассинхронизации с источником истины бесплатно. Презумпция против поля: пока нет доказанного читателя — поле не нужно (YAGNI), бремя доказательства на авторе. Денормализация оправдана только когда LINQ-проекция дорога и индекс невозможен
Связанные ловушки: маппинг owned-types —
dex-skill-dotnet-ef-core(«Owned-Type из одного значимого поля»); проекция в read-модели вместо хранения —dex-skill-clean-architecture(«Repository возвращает Entity вместо проекции для read-моделей»).
Плохо: Entity с только get; set;, вся логика в OrderService.Cancel(order)
Правильно: order.Cancel() — логика внутри Entity, сервис только оркестрирует
Почему: бизнес-правила размазаны по сервисам, дублируются, легко обойти. Entity = данные без поведения = структура, не объект
Плохо: public OrderStatus Status { get; set; } — кто угодно меняет статус
Правильно: public OrderStatus Status { get; private set; } + метод order.Cancel()
Почему: переход Submitted → Cancelled допустим, Delivered → Submitted — нет. Без инкапсуляции инварианты состояния не защищены
Плохо: public class Money { public decimal Amount { get; set; } }
Правильно: public record Money(decimal Amount, string Currency)
Почему: Value Object с set теряет гарантию equality-by-value. Два объекта равны → один мутировал → второй "тоже изменился" в коллекциях/словарях
Плохо: new Email("") или new Money(-100, "") — создаётся невалидный объект
Правильно: валидация в конструкторе, throw при невалидных данных
Почему: "always valid" — главная гарантия Value Object. Если можно создать невалидный — проверки расползаются по всему коду
Плохо: публикация OrderCreatedEvent → подписчик запрашивает Order → его ещё нет в БД
Правильно: collect events → SaveChangesAsync() → dispatch events
Почему: подписчик получает событие о несуществующих данных, race condition между publish и persist
Плохо: OrderCreatedHandler отправляет email без проверки "уже отправлен?"
Правильно: handler проверяет идемпотентность (по EventId или бизнес-ключу)
Почему: при retry (сбой после dispatch, до commit) событие обработается повторно — двойной email, двойное списание
Плохо: record OrderCreatedEvent(Order Order) — передаёт весь Aggregate
Правильно: record OrderCreatedEvent(int OrderId, DateTime CreatedAt) — только ID и нужные данные
Почему: Event = контракт. Изменение Entity ломает всех подписчиков, сериализация тянет граф объектов, нарушает bounded context
Плохо: IOrderItemRepository — отдельный репозиторий для вложенной сущности
Правильно: доступ к OrderItem только через IOrderRepository → order.AddItem()
Почему: OrderItem без Order не имеет смысла. Отдельный репозиторий позволяет обходить инварианты Aggregate Root
Плохо: OrderRepository.CreateOrderWithDiscount() — расчёт скидки в Repository
Правильно: Repository = CRUD для Aggregate, логика в Domain/Application
Почему: бизнес-логика привязана к persistence layer, при смене хранилища теряется
Плохо: один термин обозначает разные сущности в одном bounded context (имя совпадает между Entity, репозиторием VCS, DTO или ролью), либо один тип имени используется для разных ID
Правильно: каждая сущность получает уникальное имя с контекстным префиксом (CustomerId / VendorId, не просто Id), отражающее роль в домене
Почему: смешение одноимённых сущностей ведёт к тихим багам на стыках слоёв — разработчик читает имя, предполагает одну сущность, а в рантайме работает другая. Technical-корректное имя, путающее в домене — это баг, а не стилистика
Плохо: string Id, string Name, int Value в DTO / параметрах
Правильно: CustomerId, ProductName, OrderTotalCents — имя несёт доменный смысл
Почему: generic-имена приводят к путанице при рефакторинге (Id одного типа подставляется вместо Id другого, компилятор не ловит — строки / int совпадают). Доменное имя работает как тип-контракт на уровне read-only документации
Плохо: DTO локального домена использует имена полей внешнего API as-is (SolutionPath от интегрируемого сервиса = путь к файлу в локальном словаре? или ID проекта во внешней системе?)
Правильно: на границе интеграции имя мапится в локальный ubiquitous language (SolutionPath → ExternalProjectIdentifier в локальном словаре)
Почему: имя внешнего API отражает ЕГО домен, не твой. Прямое заимствование притаскивает чужие понятия в твой код и превращает DTO в leak интеграции. Через 3 месяца никто не помнит, что SolutionPath — это не путь
Плохо: создать новый класс / DTO с именем, которое уже занято в домене другой сущностью (второй Project рядом с существующим Entity Project)
Правильно: проверить домен перед введением имени, использовать уточняющий префикс (ProjectSummaryDto, ProjectCreateRequest)
Почему: два объекта с одинаковым именем в одном bounded context → в коде using X.Y vs using X.Z становится критичным, IntelliSense выбирает случайный вариант, поиск по имени теряет смысл. Squatting разрушает ubiquitous language постепенно и незаметно
Плохо: публичное свойство Response/DTO названо по способу получения данных или по флагу внутренней ветки кода (HasImplementation, UseFallbackPath, IsLegacyMode), хотя по бизнесу оно означает что-то конкретное (например, «у сотрудника есть онбординг», «расчёт пошёл по упрощённой формуле»)
Правильно: имя отражает доменное значение, которое поле несёт для потребителя API (HasOnboarding, IsRoughEstimate). Если значение — флаг, имя описывает событие/состояние домена, а не имя if-ветки в хендлере
Почему: Response — контракт с внешним потребителем; имя по реализации делает поле непрозрачным (чтобы понять смысл, нужно читать handler) и устаревает при рефакторинге внутренней ветки, оставляя контракт врущим
Плохо: AppDbContext с 50 DbSet — все доменные модели в одном контексте
Правильно: OrderDbContext, IdentityDbContext, CatalogDbContext — по bounded context
Почему: изменение одной модели = миграция всего контекста, конфликты между командами, медленная инициализация
Плохо: PricingService хранит кэш цен в поле, зарегистрирован как Scoped
Правильно: Domain Service — stateless, данные получает через параметры или Repository
Почему: состояние в сервисе = скрытая зависимость, проблемы с concurrency, непредсказуемое поведение при DI lifetime
Плохо: new OrderByCustomerSpecification(customerId) для WHERE CustomerId = @id
Правильно: Specification — для сложных составных фильтров, простые запросы — метод в Repository
Почему: overhead абстракции без выгоды. Specification оправдан при комбинируемых фильтрах (UI-грид с 10 фильтрами), не для одного WHERE
Id / Name без префиксаnpx claudepluginhub dex-it/claude-code-marketplace --plugin dex-skill-dddCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.