Реализация 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 — без общего языка команда будет путаться в терминах.
- Игнорирование тестирования — без модульных тестов теряется смысл изоляции логики.
Закажите консультацию для оценки вашего проекта — мы подберём оптимальный объём внедрения и спланируем дорожную карту. Свяжитесь с нами для бесплатного аудита текущей архитектуры. Наши инженеры гарантируют качество и соблюдение сроков.







