PurpleSchool — курсы программирования онлайн
  • Пути
    • Frontend React разработчик
    • Frontend Vue разработчик
    • Backend разработчик Node.js
    • Fullstack разработчик React / Node.js
    • Mobile разработчик React Native
    • Backend разработчик Golang
    • Devops инженер
    • Backend разработчик Python
  • AI для кодаНовое
  • О нас
    • Отзывы
    • Реферальная программа
    • О компании
    • Контакты
  • Иконка открытия меню
    • Сообщество
    • PurpleПлюс
    • AI Собеседование
    • AI тренажёр
    • Проекты
PurpleSchool — платформа бесплатных roadmap и курсов для разработчиков
ютуб иконка
Telegram иконка
VK иконка
VK иконка
Курсы
ГлавнаяКаталог курсовFrontendBackendFullstack
Практика
КарьераПроектыPurpleПлюс
Материалы
БлогБаза знаний
Документы
Договор офертаПолитика конфиденциальностиПроверка сертификатаМиграция курсовРеферальная программа
Реквизиты
ИП Ларичев Антон АндреевичИНН 773373765379contact@purpleschool.ru

PurpleSchool © 2020 -2026 Все права защищены

  • Курсы
    • FrontendИконка стрелки
    • AI разработкаИконка стрелки
    • BackendИконка стрелки
    • DevOpsИконка стрелки
    • MobileИконка стрелки
    • ТестированиеИконка стрелки
    • Soft-skillsИконка стрелки
    • ДизайнИконка стрелки
    Иконка слояПерейти в каталог курсов
  • Бесплатно
    • Курсы
    • JavaScript Основы разработкиPython Основы PythonCSS CSS FlexboxКарта развитияВопросы для собеседований
    • База знанийИконка стрелки
    • Новостные рассылкиИконка стрелки
  • PurpleSchool — курсы программирования онлайн
    • AI для кодаНовое
    • Сообщество
    • PurpleПлюс
    • AI Собеседование
    • AI тренажёр
    • Проекты
    Главная
    Сообщество
    Domain-Driven Design на TypeScript: практическое руководство

    Domain-Driven Design на TypeScript: практическое руководство

    Аватар автора Domain-Driven Design на TypeScript: практическое руководство

    Антон Ларичев

    Иконка календаря14 июля 2026
    TypeScriptDDDархитектураDomain-Driven Designпаттерны проектированияseniorИконка уровня senior
    Картинка поста Domain-Driven Design на TypeScript: практическое руководство

    Введение

    Domain-Driven Design (DDD) — подход к разработке, при котором модель предметной области становится центром архитектуры. TypeScript с его строгой типизацией идеально подходит для реализации принципов DDD: типы помогают выразить доменные концепции явно и защитить инварианты прямо на уровне компилятора.

    В этом руководстве разберём ключевые строительные блоки DDD и реализуем их на примере системы управления заказами.

    Объекты-значения (Value Objects)

    Объект-значение не имеет идентичности — два объекта с одинаковыми данными считаются равными. Они всегда иммутабельны.

    class Money {
      private constructor(
        private readonly amount: number,
        private readonly currency: string
      ) {
        // сумма не может быть отрицательной
        if (amount < 0) throw new Error('Сумма не может быть отрицательной');
      }
    
      static create(amount: number, currency: string): Money {
        return new Money(amount, currency);
      }
    
      add(other: Money): Money {
        if (this.currency !== other.currency) {
          throw new Error('Нельзя складывать разные валюты');
        }
        return new Money(this.amount + other.amount, this.currency);
      }
    
      equals(other: Money): boolean {
        return this.amount === other.amount && this.currency === other.currency;
      }
    
      getAmount(): number { return this.amount; }
      getCurrency(): string { return this.currency; }
    }
    

    Приватный конструктор и фабричный метод create гарантируют проверку инвариантов в одной точке. Внешний код не может создать невалидный объект в обход этой проверки.

    Сущности (Entities)

    Сущность имеет уникальный идентификатор. Два объекта с одинаковыми данными, но разными ID — это разные сущности.

    class OrderId {
      constructor(private readonly value: string) {
        if (!value) throw new Error('ID заказа не может быть пустым');
      }
      getValue(): string { return this.value; }
      equals(other: OrderId): boolean { return this.value === other.value; }
    }
    
    class OrderItem {
      constructor(
        private readonly productId: string,
        private readonly quantity: number,
        private readonly price: Money
      ) {
        // количество должно быть строго положительным
        if (quantity <= 0) throw new Error('Количество должно быть больше нуля');
      }
    
      getTotalPrice(): Money {
        return Money.create(
          this.price.getAmount() * this.quantity,
          this.price.getCurrency()
        );
      }
    }
    

    Агрегаты (Aggregates)

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

    type OrderStatus = 'pending' | 'confirmed' | 'shipped' | 'cancelled';
    
    class Order {
      private items: OrderItem[] = [];
      private status: OrderStatus = 'pending';
      private readonly domainEvents: DomainEvent[] = [];
    
      private constructor(private readonly id: OrderId) {}
    
      static create(id: OrderId): Order {
        const order = new Order(id);
        // публикуем событие о создании заказа
        order.addEvent(new OrderCreatedEvent(id.getValue()));
        return order;
      }
    
      addItem(item: OrderItem): void {
        if (this.status !== 'pending') {
          throw new Error('Нельзя добавить товар в уже подтверждённый заказ');
        }
        this.items.push(item);
      }
    
      confirm(): void {
        if (this.items.length === 0) {
          throw new Error('Нельзя подтвердить пустой заказ');
        }
        this.status = 'confirmed';
        this.addEvent(new OrderConfirmedEvent(this.id.getValue()));
      }
    
      getTotalAmount(): Money {
        return this.items.reduce(
          (total, item) => total.add(item.getTotalPrice()),
          Money.create(0, 'RUB')
        );
      }
    
      private addEvent(event: DomainEvent): void {
        this.domainEvents.push(event);
      }
    
      getUncommittedEvents(): DomainEvent[] { return [...this.domainEvents]; }
      clearEvents(): void { this.domainEvents.length = 0; }
      getId(): OrderId { return this.id; }
    }
    

    Доменные события (Domain Events)

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

    interface DomainEvent {
      readonly occurredAt: Date;
      readonly eventType: string;
    }
    
    class OrderCreatedEvent implements DomainEvent {
      readonly occurredAt = new Date();
      readonly eventType = 'OrderCreated';
      constructor(readonly orderId: string) {}
    }
    
    class OrderConfirmedEvent implements DomainEvent {
      readonly occurredAt = new Date();
      readonly eventType = 'OrderConfirmed';
      constructor(readonly orderId: string) {}
    }
    

    Репозитории (Repositories)

    Репозиторий абстрагирует работу с хранилищем. В доменном слое живёт только интерфейс — реализация находится в инфраструктурном слое.

    interface OrderRepository {
      findById(id: OrderId): Promise<Order | null>;
      save(order: Order): Promise<void>;
      nextId(): OrderId;
    }
    
    // реализация в инфраструктурном слое
    class InMemoryOrderRepository implements OrderRepository {
      private orders = new Map<string, Order>();
    
      async findById(id: OrderId): Promise<Order | null> {
        return this.orders.get(id.getValue()) ?? null;
      }
    
      async save(order: Order): Promise<void> {
        this.orders.set(order.getId().getValue(), order);
        // здесь публикуем доменные события через шину событий
        order.clearEvents();
      }
    
      nextId(): OrderId {
        return new OrderId(crypto.randomUUID());
      }
    }
    

    Доменные службы (Domain Services)

    Если бизнес-логика не принадлежит ни одной сущности — она выносится в доменную службу.

    class OrderPricingService {
      calculateDiscount(order: Order, customerTier: string): Money {
        const total = order.getTotalAmount();
        // скидка 10% для VIP-клиентов
        if (customerTier === 'vip') {
          return Money.create(total.getAmount() * 0.1, total.getCurrency());
        }
        return Money.create(0, total.getCurrency());
      }
    }
    

    Частые ошибки

    Анемичная модель домена. Сущности содержат только геттеры и сеттеры, вся логика вынесена в сервисы. Это антипаттерн: инварианты должны защищаться внутри агрегатов, а не снаружи.

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

    Утечка доменной логики в прикладной слой. Application Services оркестрируют, но не содержат бизнес-правила. Условия if/else на основе бизнес-правил в UseCase — сигнал переместить логику в домен.

    Слишком большие агрегаты. Одна транзакция должна изменять один агрегат. Стремитесь к минимально возможному размеру.

    Отсутствие единого языка (Ubiquitous Language). Если имена классов расходятся с терминами бизнеса, команда постоянно теряет время на «перевод» между кодом и требованиями.

    Заключение

    Domain-Driven Design на TypeScript даёт мощный инструментарий для моделирования сложных предметных областей. Строгая типизация усиливает DDD: Value Objects защищают инварианты, Aggregates инкапсулируют бизнес-правила, Domain Events делают систему реактивной и слабосвязанной.

    Начните с небольшого Bounded Context, выстройте единый язык с командой, и DDD из теоретической концепции превратится в рабочий инструмент для решения реальных задач.

    Иконка глаза3

    Комментарии

    0

    Постройте личный план изучения HTML и CSS - полный курс по вёрстке с нуля до уровня Middle — бесплатно!

    HTML и CSS - полный курс по вёрстке с нуля — часть карты развития Frontend, Mobile

    • step100+ шагов развития
    • lessons30 бесплатных лекций
    • lessons300 бонусных рублей на счет

    Бесплатные лекции

    Лучшие курсы по теме

    изображение курса

    CSS Flexbox

    Антон Ларичев
    Гарантия
    Бонусы
    иконка звёздочки рейтинга4.9
    бесплатно
    Подробнее
    изображение курса

    Основы JavaScript

    Антон Ларичев
    AI-тренажерыAI-тренажеры
    Практика в студииПрактика в студии
    Гарантия
    Бонусы
    иконка звёздочки рейтинга4.8
    3 999 ₽ 6 990 ₽
    Подробнее
    изображение курса

    Продвинутый JavaScript

    Антон Ларичев
    AI-тренажерыAI-тренажеры
    Практика в студииПрактика в студии
    Гарантия
    Бонусы
    иконка звёздочки рейтинга4.8
    3 999 ₽ 6 990 ₽
    Подробнее

    Похожие статьи

    Картинка поста Kafka и Node.js: Event-Driven архитектура на практике
    Иконка аватараАнтон
    Иконка календаря13 июля 2026
    kafkanodejsevent-driven+ 2seniorИконка уровня senior

    Kafka и Node.js: Event-Driven архитектура на практике

    Event-Driven архитектура с Kafka в Node.js: продюсеры, консьюмеры, гарантии доставки и типичные ошибки на реальных примерах.

    Иконка чипа0
    Иконка глаза44
    Иконка комментариев0
    Картинка поста Микрофронтенды и Module Federation: подходы и реализация
    Иконка аватараАнтон
    Иконка календаря06 июля 2026
    микрофронтендыmodule-federationwebpack+ 2seniorИконка уровня senior

    Микрофронтенды и Module Federation: подходы и реализация

    Микрофронтенды с Module Federation: как разбить монолитный фронтенд на независимые части, которые деплоятся и разрабатываются отдельно.

    Иконка чипа0
    Иконка глаза188
    Иконка комментариев0
    Картинка поста Clean Architecture: чистая архитектура для веб-разработки
    Иконка аватараАнтон
    Иконка календаря02 июля 2026
    архитектураclean architecturetypescript+ 3middleИконка уровня middle

    Clean Architecture: чистая архитектура для веб-разработки

    Clean Architecture разделяет код на независимые слои: бизнес-логика не зависит от фреймворков, БД и внешних сервисов. Разбираем на примерах TypeScript.

    Иконка чипа0
    Иконка глаза232
    Иконка комментариев0
    Иконка чипа0