Реализация Domain-Driven Design (DDD) для веб-приложения

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация Domain-Driven Design (DDD) для веб-приложения
Сложная
от 2 недель до 3 месяцев
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Реализация Domain-Driven Design (DDD) для веб-приложения

DDD — методология проектирования ПО, при которой архитектура системы отражает бизнес-домен. Код говорит на том же языке, что и эксперты предметной области. Не «обновить запись в таблице users», а «заблокировать аккаунт за нарушение политики». DDD оправдан для сложных доменов — e-commerce с нетривиальными правилами ценообразования, финансовых систем, SaaS с гибкими тарифами.

Ubiquitous Language

Первый шаг — создание единого словаря с бизнес-экспертом. Термины кодируются в код напрямую.

Плохо: updateUserStatus(userId, 1) или setActive(true) Хорошо: customer.activate(), subscription.suspend(reason), order.submit()

Словарь фиксируется в документе и поддерживается в актуальности при изменении требований.

Bounded Context

Большую систему делят на Bounded Context — ограниченные контексты, в каждом из которых термины имеют строго определённый смысл. «Продукт» в контексте Каталога — это описание, фото, SEO. «Продукт» в контексте Заказов — SKU, цена, количество. «Продукт» в контексте Склада — физическая единица с местом хранения.

┌─────────────────────┐  ┌─────────────────────┐  ┌─────────────────────┐
│   Catalog Context   │  │   Orders Context    │  │ Inventory Context   │
│                     │  │                     │  │                     │
│ Product             │  │ OrderItem           │  │ StockItem           │
│ Category            │  │ Order               │  │ Warehouse           │
│ PriceList           │  │ Customer            │  │ StockMovement       │
└─────────────────────┘  └─────────────────────┘  └─────────────────────┘
         ↑ Anti-Corruption Layer между контекстами

Между контекстами — явные границы и маппинг. Context Map документирует взаимодействие.

Строительные блоки

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()));
  }

  get id(): OrderId { return this._id; }
  get total(): Money { return this.calculateTotal(); }
  pullDomainEvents(): DomainEvent[] { /* ... */ }
}

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);
  }

  multiply(factor: number): Money {
    return new Money(Math.round(this._amount * factor * 100) / 100, this._currency);
  }

  equals(other: Money): boolean {
    return this._amount === other._amount && this._currency.equals(other._currency);
  }
}

class Email {
  private constructor(private readonly value: string) {}

  static create(email: string): Email {
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      throw new InvalidEmailError(email);
    }
    return new Email(email.toLowerCase());
  }
}

Aggregate — кластер связанных сущностей с единой точкой входа (Aggregate Root). Внешний код работает только с корнем агрегата:

// Order — Aggregate Root
// OrderItem — входит в агрегат Order, не доступен напрямую снаружи
class Order {
  // ...всё взаимодействие с items только через Order
  removeItem(productId: ProductId): void { /* ... */ }
  updateQuantity(productId: ProductId, qty: Quantity): void { /* ... */ }
}

Правило: одна транзакция = один агрегат. Между агрегатами — eventual consistency через доменные события.

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>;
  delete(id: OrderId): 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;
  }

  private toDomain(row: OrderRow): Order {
    return Order.reconstitute({
      id: OrderId.from(row.id),
      status: OrderStatus[row.status],
      customerId: CustomerId.from(row.customer_id),
      items: row.items.map(this.itemToDomain)
    });
  }
}

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 };
  }
}

Слоистая архитектура

Domain Layer          — Entities, Value Objects, Aggregates, Domain Services, Events
Application Layer     — Use Cases, Application Services, DTOs, Ports
Infrastructure Layer  — Repositories (PostgreSQL/Redis), External APIs, Message Brokers
Presentation Layer    — HTTP Controllers, GraphQL Resolvers, CLI Commands

Зависимости направлены только внутрь: Presentation → Application → Domain. Domain не зависит ни от чего.

Сроки реализации

  • Проектирование домена, Context Map, Ubiquitous Language — 1–2 недели
  • Реализация одного Bounded Context с 3–5 агрегатами — 3–5 недель
  • Полная система с несколькими контекстами и интеграцией — 2–4 месяца