Реалізація Domain-Driven Design (DDD) для веб-застосунку

Реалізація Domain-Driven Design (DDD) для веб-застосунку

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Реалізація Domain-Driven Design (DDD) для веб-застосунку
Складний
від 2 тижнів до 3 місяців

Наші компетенції:

Часті запитання

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1467
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1318
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1015
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1276
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1019
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1019

Реалізація 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

  1. Виділіть найскладніший bounded context (наприклад, управління замовленнями).
  2. Визначте ubiquitous language з бізнес-експертами — запишіть усі терміни.
  3. Спроєктуйте агрегати та value objects, зафіксуйте інваріанти.
  4. Напишіть модульні тести для доменної логіки (покриття 90%+).
  5. Інтегруйте контексти через доменні події та антикорупційний шар.
Кейс: інтернет-магазин електроніки У проєкті з продажу електроніки ми виділили контекст 'Управління замовленнями'. Складність була в гнучких правилах знижок та акцій. Ми спроєктували агрегат 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 — без спільної мови команда буде плутатися в термінах.
  • Ігнорування тестування — без модульних тестів втрачається сенс ізоляції логіки.

Замовте консультацію для оцінки вашого проєкту — ми підберемо оптимальний обсяг впровадження та сплануємо дорожню карту. Зв'яжіться з нами для безкоштовного аудиту поточної архітектури. Наші інженери гарантують якість та дотримання термінів.