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 из теоретической концепции превратится в рабочий инструмент для решения реальных задач.

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

    Комментарии

    0

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

    Neovim - практика и настройка — часть карты развития Frontend, Backend, DevOps

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

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

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

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

    React и Redux Toolkit

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

    Zustand

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

    Next.js - с нуля

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

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

    Картинка поста Микросервисная архитектура для начинающих: гайд с примерами
    Иконка аватараАнтон
    Иконка календаря23 сентября 2026
    микросервисыархитектураbackend+ 2juniorИконка уровня junior

    Микросервисная архитектура для начинающих: гайд с примерами

    Микросервисная архитектура простыми словами: чем она отличается от монолита, какие есть плюсы и минусы, и как спроектировать первую систему на микросервисах — с примерами кода.

    Иконка чипа0
    Иконка глаза243
    Иконка комментариев0
    Картинка поста CLAUDE.md: как написать инструкции для Claude Code
    Иконка аватараАнтон
    Иконка календаря11 октября 2026
    AI-инструментыClaude CodeDevOps+ 1middleИконка уровня middle

    CLAUDE.md: как написать инструкции для Claude Code

    CLAUDE.md — это файл с инструкциями по проекту, который Claude Code читает в начале каждой сессии. Положите его в корень репозитория и опишите внутри, чем проект собирается, чем запускаются тесты и каких соглашений держаться в коде. После этого агент перестанет спрашивать одно и то же в каждом новом диалоге и предлагать решения, которые в вашей кодовой базе не приняты. Формат предельно простой: обычный Markdown, обязательных полей нет, заголовки любые. Дальше — где именно создавать файл и как Claude Code собирает инструкции сразу из нескольких мест, что писать внутрь, как подключать к файлу другие документы и чем CLAUDE.md отличается от AGENTS.md. Одна оговорка перед началом: Claude Code обновляется часто, набор команд и поддерживаемых файлов пополняется. Если какая-то деталь ниже ведёт себя у вас иначе, сверяйтесь с документацией той версии, которая стоит у вас.

    Иконка чипа0
    Иконка глаза79
    Иконка комментариев0
    Иконка аватараАнтон
    Иконка календаря10 октября 2026
    AI-инструментыClaude CodeDevOps+ 2middleИконка уровня middle

    Агенты в OpenCode: как создать своего агента

    Агент в OpenCode — это именованный профиль работы: своя модель, свой системный промпт и свой список разрешённых инструментов. Чтобы создать своего агента, нужно описать его отдельным markdown-файлом в папке `.opencode/agent/` внутри проекта (или в `~/.config/opencode/agent/`, если агент нужен во всех проектах) либо объявить его в блоке `agent` файла `opencode.json`. После этого агент появляется в списке доступных, и его можно вызвать прямо в сессии.

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