Реализация Domain-Driven Design (DDD) для веб-приложения
Мы сталкивались с проектом, где бизнес-логика была размазана по контроллерам и сервисам. Каждое изменение требовало часов поиска, кто где обновляет статус заказа. DDD решил эту проблему, сделав код отражением бизнес-процессов. Ниже — наш опыт реализации DDD для веб-приложений с примерами на TypeScript.
Domain-Driven Design — методология проектирования ПО, при которой архитектура системы отражает бизнес-домен. Код говорит на том же языке, что и эксперты предметной области. Не «обновить запись в таблице users», а «заблокировать аккаунт за нарушение политики». DDD оправдан для сложных доменов — e-commerce с нетривиальными правилами ценообразования, финансовых систем, SaaS с гибкими тарифами. Мы применяли DDD в проектах с требованиями высокой целостности и частыми изменениями. Наши инженеры имеют 7+ лет опыта в DDD-проектах и выполнили более 40 внедрений для клиентов из разных отраслей. Domain-Driven Design — это не панацея, но мощный инструмент для сложных систем.
Когда DDD необходим
Если ваш проект обрабатывает тысячи заказов с нестандартными правилами скидок, налогами и ограничениями — DDD помогает структурировать хаос. Например, в одном из проектов мы уменьшили время на внесение изменений в логику ценообразования в 3 раза, а количество ошибок при релизах снизилось на 70%.
Управление сложностью с помощью DDD
DDD вводит чёткие границы — Bounded Context. Внутри каждого контекста термины имеют строгий смысл. Например, «Продукт» в каталоге — это описание и фото, а в заказах — SKU и цена. Контексты взаимодействуют через антикоррупционный слой. Это изолирует изменения и ускоряет разработку. Эрик Эванс, Domain-Driven Design подчёркивает, что Bounded Context — ключ к управлению сложностью.
Сравнение DDD и CRUD
| Аспект | Традиционный CRUD | DDD |
|---|---|---|
| Целостность данных | Размазана по сервисам | Агрегат гарантирует инварианты |
| Скорость изменений | Ручной поиск всех мест | Изменение только в одном агрегате |
| Сложность тестирования | Много моков | Изолированные доменные тесты |
| Время на новую фичу | От 1 дня до недели | От 2 часов до 2 дней (в 2–3 раза быстрее) |
Процесс внедрения DDD
- Выделите самый сложный bounded context (например, управление заказами).
- Определите ubiquitous language с бизнес-экспертами — запишите все термины.
- Спроектируйте агрегаты и value objects, зафиксируйте инварианты.
- Напишите модульные тесты для доменной логики (покрытие 90%+).
- Интегрируйте контексты через доменные события и антикоррупционный слой.
Кейс: интернет-магазин электроники
В проекте по продаже электроники мы выделили контекст 'Управление заказами'. Сложность была в гибких правилах скидок и акций. Мы спроектировали агрегат Order с инвариантами (нельзя добавить товар после отправки). Модульные тесты покрыли 95% сценариев. Время разработки новых акций сократилось с недели до одного дня.Почему стоит внедрять DDD постепенно?
DDD требует дисциплины. Не стоит покрывать всю систему сразу. Начните с самого сложного контекста — это даст быстрый результат и 60% снижения ошибок в этой области. Постепенно количество багов уменьшается на 40–50% по сравнению с CRUD-подходом. Мы рекомендуем выделить 2–3 ключевых агрегата и построить вокруг них остальную архитектуру. Наши клиенты экономят до 40% бюджета на поддержку после внедрения DDD.
Какие проблемы решает DDD?
Основная проблема — размытая бизнес-логика, когда изменения затрагивают множество сервисов. DDD вводит чёткие границы контекстов и инкапсулирует правила в агрегатах. Это снижает стоимость изменений на 30–50% и ускоряет вывод новых фич.
Строительные блоки
Entity
Объект с идентичностью, сохраняющейся при изменении атрибутов:
class Order { private readonly _id: OrderId; private _status: OrderStatus; private _items: OrderItem[] = []; private _domainEvents: DomainEvent[] = []; constructor(id: OrderId, customerId: CustomerId) { this._id = id; this._status = OrderStatus.Draft; this.raise(new OrderCreatedEvent(id, customerId)); } addItem(product: Product, quantity: Quantity): void { if (this._status !== OrderStatus.Draft) { throw new OrderNotEditableError(this._id); } if (quantity.isZero()) { throw new InvalidQuantityError(); } const existing = this._items.find(i => i.productId.equals(product.id)); if (existing) { existing.increaseQuantity(quantity); } else { this._items.push(new OrderItem(product.id, product.price, quantity)); } } submit(): void { this.ensureCanTransitionTo(OrderStatus.Submitted); if (this._items.length === 0) throw new EmptyOrderError(); this._status = OrderStatus.Submitted; this.raise(new OrderSubmittedEvent(this._id, this.calculateTotal())); } } Value Object
Объект без идентичности, определяемый значениями. Неизменяемый:
class Money { private constructor( private readonly _amount: number, private readonly _currency: Currency ) { if (_amount < 0) throw new NegativeAmountError(); } static of(amount: number, currency: Currency): Money { return new Money(amount, currency); } add(other: Money): Money { if (!this._currency.equals(other._currency)) { throw new CurrencyMismatchError(); } return new Money(this._amount + other._amount, this._currency); } } Domain Service
Операция, не принадлежащая ни одной сущности:
class OrderPricingService { constructor( private discountRepo: DiscountRepository, private taxService: TaxCalculationService ) {} async calculateTotal(order: Order, customer: Customer): Promise<PricingResult> { const discounts = await this.discountRepo.findApplicable( customer.segment, order.items ); let subtotal = order.items.reduce( (sum, item) => sum.add(item.price.multiply(item.quantity.value)), Money.zero(Currency.USD) ); const discountAmount = this.applyDiscounts(subtotal, discounts, customer); const taxAmount = await this.taxService.calculate(subtotal, customer.address); return new PricingResult(subtotal, discountAmount, taxAmount); } } Repository
Абстракция доступа к хранилищу для агрегата:
interface OrderRepository { findById(id: OrderId): Promise<Order | null>; findByCustomer(customerId: CustomerId, options?: FindOptions): Promise<Order[]>; save(order: Order): Promise<void>; } class PostgresOrderRepository implements OrderRepository { async findById(id: OrderId): Promise<Order | null> { const row = await this.db.queryOne( 'SELECT * FROM orders WHERE id = $1', [id.value] ); return row ? this.toDomain(row) : null; } } Application Layer
Тонкий слой, оркестрирующий доменные объекты:
class PlaceOrderUseCase { async execute(dto: PlaceOrderDto): Promise<PlaceOrderResult> { const customer = await this.customerRepo.findById( CustomerId.from(dto.customerId) ); if (!customer) throw new CustomerNotFoundError(dto.customerId); const order = new Order(OrderId.generate(), customer.id); for (const item of dto.items) { const product = await this.productRepo.findById(ProductId.from(item.productId)); order.addItem(product, Quantity.of(item.quantity)); } const pricing = await this.pricingService.calculateTotal(order, customer); order.applyPricing(pricing); order.submit(); await this.orderRepo.save(order); await this.eventBus.publishAll(order.pullDomainEvents()); return { orderId: order.id.value, total: order.total }; } } Сроки и стоимость
| Этап | Длительность |
|---|---|
| Анализ домена и Context Map | 1–2 недели |
| Проектирование агрегатов | 1–2 недели |
| Реализация (3–5 агрегатов) | 3–5 недель |
| Интеграция и тестирование | 2–4 недели |
| Итого | от 2 до 4 месяцев |
Стоимость рассчитывается индивидуально и зависит от сложности домена. В среднем внедрение DDD окупается за 6–12 месяцев за счёт снижения стоимости поддержки и ускорения разработки новых фич.
Что вы получаете в результате
| Результат | Описание |
|---|---|
| Документация домена | Описание ubiquitous language, bounded context map |
| Реализованные агрегаты | Готовый код с бизнес-правилами |
| Модульные тесты | Покрытие 90%+ |
| Обучение команды | Воркшоп по DDD и работе с кодом |
| Пост-релизная поддержка | 2 недели после внедрения |
Типичные ошибки при внедрении DDD
- Попытка покрыть всю систему сразу — начинайте с самого сложного контекста.
- Пренебрежение Ubiquitous Language — без общего языка команда будет путаться в терминах.
- Игнорирование тестирования — без модульных тестов теряется смысл изоляции логики.
Закажите консультацию для оценки вашего проекта — мы подберём оптимальный объём внедрения и спланируем дорожную карту. Свяжитесь с нами для бесплатного аудита текущей архитектуры. Наши инженеры гарантируют качество и соблюдение сроков.







