Home > Blog > tech

Domain-Driven Design (DDD) คืออะไร? สอนออกแบบ Software ตามโดเมนธุรกิจ 2026

domain driven design ddd guide
Domain-Driven Design DDD Guide 2026
2026-04-10 | tech | 3600 words

เมื่อ Software มีความซับซ้อนมากขึ้น การเขียน Code แบบ CRUD ธรรมดาไม่เพียงพออีกต่อไป Business Logic ที่ซับซ้อน กฎเกณฑ์ที่เปลี่ยนแปลงตลอด และระบบที่ต้องขยายตัว ทำให้ต้องมีวิธีออกแบบ Software ที่ดีกว่า Domain-Driven Design (DDD) คือคำตอบ

บทความนี้จะพาเรียนรู้ DDD ตั้งแต่แนวคิดพื้นฐาน Strategic Design ไปจนถึง Tactical Patterns พร้อมตัวอย่าง Code จริงในปี 2026

DDD คืออะไร?

Domain-Driven Design (DDD) คือแนวทางการออกแบบ Software ที่เน้นให้ Code สะท้อนโดเมนธุรกิจ (Business Domain) อย่างแท้จริง นำเสนอครั้งแรกโดย Eric Evans ในหนังสือ "Domain-Driven Design: Tackling Complexity in the Heart of Software" (2003)

แนวคิดหลักของ DDD:

Strategic Design — ภาพใหญ่

Ubiquitous Language

Ubiquitous Language คือภาษากลางที่ทีมทั้งหมดใช้ร่วมกัน ทั้ง Developer, Domain Expert, QA และ PM:

// BAD: ภาษาของ Developer ไม่ตรงกับธุรกิจ
class DataProcessor {
  processItem(data: any) {
    if (data.flag === 1) {
      this.updateRecord(data);
    }
  }
}

// GOOD: ใช้ Ubiquitous Language
class OrderService {
  confirmOrder(order: Order) {
    if (order.isPaid()) {
      order.confirm();
      this.notifyWarehouse(order);
    }
  }
}
หลักสำคัญ: เมื่อ Domain Expert พูดว่า "ยืนยันออเดอร์" Code ต้องมี method ชื่อ confirmOrder() ไม่ใช่ processData() หรือ updateStatus()

Bounded Context

Bounded Context คือขอบเขตที่ Model มีความหมายชัดเจน ใน Context หนึ่ง "Customer" อาจหมายถึงสิ่งที่ต่างจากอีก Context:

// Sales Context: Customer = ผู้ซื้อ มี Order History
class Customer {
  id: CustomerId;
  name: string;
  orders: Order[];
  loyaltyPoints: number;
  placeOrder(items: CartItem[]): Order { ... }
}

// Support Context: Customer = ผู้ร้องเรียน มี Ticket History
class Customer {
  id: CustomerId;
  name: string;
  tickets: SupportTicket[];
  satisfactionScore: number;
  openTicket(issue: string): SupportTicket { ... }
}

// Shipping Context: Customer = ผู้รับพัสดุ มีที่อยู่
class Recipient {
  id: CustomerId;
  name: string;
  shippingAddress: Address;
  contactPhone: string;
}

Context Mapping

ความสัมพันธ์ระหว่าง Bounded Contexts:

Patternคำอธิบายตัวอย่าง
Shared Kernelแชร์ Model บางส่วนร่วมกันใช้ User ID ร่วมกัน
Customer-SupplierContext หนึ่งให้ข้อมูล อีกอันรับOrder ส่งข้อมูลให้ Shipping
Anti-Corruption Layerแปลง Model ระหว่าง ContextIntegration กับ Legacy System
Conformistใช้ Model ของอีก Context ตรงๆใช้ API ของ Third Party
Open Host Serviceเปิด API ให้ Context อื่นใช้REST/gRPC Service
Published Languageใช้ภาษากลางในการสื่อสารJSON Schema, Protobuf

Tactical Design — Pattern ในระดับ Code

Entity

Entity คือ Object ที่มี Identity ชัดเจน แม้ Attribute เปลี่ยน Identity ก็ยังเหมือนเดิม:

class Order {
  private readonly id: OrderId;
  private status: OrderStatus;
  private items: OrderItem[];
  private totalAmount: Money;

  constructor(id: OrderId, customerId: CustomerId) {
    this.id = id;
    this.status = OrderStatus.CREATED;
    this.items = [];
    this.totalAmount = Money.zero();
  }

  // Identity-based equality
  equals(other: Order): boolean {
    return this.id.equals(other.id);
  }

  addItem(product: ProductId, quantity: number, price: Money): void {
    const item = new OrderItem(product, quantity, price);
    this.items.push(item);
    this.recalculateTotal();
  }

  confirm(): void {
    if (this.items.length === 0) {
      throw new Error("Cannot confirm empty order");
    }
    this.status = OrderStatus.CONFIRMED;
  }
}

Value Object

Value Object คือ Object ที่ไม่มี Identity เปรียบเทียบกันด้วย Value ทั้งหมด:

class Money {
  private constructor(
    private readonly amount: number,
    private readonly currency: string
  ) {
    if (amount < 0) throw new Error("Amount cannot be negative");
  }

  static of(amount: number, currency: string): Money {
    return new Money(amount, currency);
  }

  static zero(): Money {
    return new Money(0, "THB");
  }

  add(other: Money): Money {
    if (this.currency !== other.currency) {
      throw new Error("Cannot add different currencies");
    }
    return new Money(this.amount + other.amount, this.currency);
  }

  multiply(factor: number): Money {
    return new Money(this.amount * factor, this.currency);
  }

  // Value-based equality
  equals(other: Money): boolean {
    return this.amount === other.amount
        && this.currency === other.currency;
  }
}

class Address {
  constructor(
    readonly street: string,
    readonly city: string,
    readonly province: string,
    readonly postalCode: string
  ) {
    // Immutable — ไม่มี setter
    Object.freeze(this);
  }
}
Entity vs Value Object: คน 2 คนชื่อเหมือนกันไม่ใช่คนเดียวกัน (Entity) แต่แบงค์ 100 บาท 2 ใบ แลกเปลี่ยนกันได้ (Value Object)

Aggregate และ Aggregate Root

Aggregate คือกลุ่มของ Entity และ Value Object ที่ต้องเปลี่ยนแปลงพร้อมกัน (Consistency Boundary) โดยมี Aggregate Root เป็นจุดเข้าถึงเดียว:

// Order เป็น Aggregate Root
// OrderItem เป็น Entity ภายใน Aggregate
class Order {  // <-- Aggregate Root
  private id: OrderId;
  private customerId: CustomerId;
  private items: OrderItem[];       // ภายใน Aggregate
  private shippingAddress: Address; // Value Object
  private status: OrderStatus;

  // ทุก Operation ต้องผ่าน Aggregate Root
  addItem(product: ProductId, qty: number, price: Money): void {
    // Business Rule: ไม่เกิน 50 items
    if (this.items.length >= 50) {
      throw new DomainError("Order cannot have more than 50 items");
    }
    this.items.push(new OrderItem(product, qty, price));
    this.recalculateTotal();
  }

  removeItem(itemId: OrderItemId): void {
    this.items = this.items.filter(i => !i.id.equals(itemId));
    this.recalculateTotal();
  }

  // ห้ามเข้าถึง OrderItem ตรงๆ จากภายนอก
  getItemCount(): number {
    return this.items.length;
  }
}
// กฎของ Aggregate:
// 1. อ้างอิงข้าม Aggregate ด้วย ID เท่านั้น (ไม่ใช่ Object Reference)
// 2. หนึ่ง Transaction = หนึ่ง Aggregate
// 3. Aggregate ต้องเล็ก (ไม่ใหญ่เกินไป)
// 4. ใช้ Domain Events สื่อสารระหว่าง Aggregate

Domain Events

Domain Events คือสิ่งที่เกิดขึ้นในโดเมนที่ Domain Expert สนใจ ใช้สื่อสารระหว่าง Aggregate:

// Domain Event
class OrderConfirmed {
  readonly occurredAt: Date;
  constructor(
    readonly orderId: OrderId,
    readonly customerId: CustomerId,
    readonly totalAmount: Money,
    readonly items: ReadonlyArray
  ) {
    this.occurredAt = new Date();
  }
}

// Aggregate Root raise event
class Order {
  private domainEvents: DomainEvent[] = [];

  confirm(): void {
    if (this.status !== OrderStatus.CREATED) {
      throw new DomainError("Can only confirm created orders");
    }
    this.status = OrderStatus.CONFIRMED;

    // Raise Domain Event
    this.domainEvents.push(new OrderConfirmed(
      this.id, this.customerId, this.totalAmount,
      this.items.map(i => i.toSnapshot())
    ));
  }

  pullDomainEvents(): DomainEvent[] {
    const events = [...this.domainEvents];
    this.domainEvents = [];
    return events;
  }
}

// Event Handler (ใน Context อื่น)
class OrderConfirmedHandler {
  async handle(event: OrderConfirmed): Promise {
    // สร้าง Shipment ใน Shipping Context
    await this.shippingService.createShipment(
      event.orderId, event.items
    );
    // ส่ง Email ยืนยัน
    await this.emailService.sendConfirmation(
      event.customerId, event.orderId
    );
  }
}

Repository Pattern

Repository ทำหน้าที่เก็บและดึง Aggregate Root จาก Storage โดยซ่อน Implementation Details:

// Repository Interface (Domain Layer)
interface OrderRepository {
  findById(id: OrderId): Promise;
  save(order: Order): Promise;
  nextId(): OrderId;
}

// Implementation (Infrastructure Layer)
class PostgresOrderRepository implements OrderRepository {
  async findById(id: OrderId): Promise {
    const row = await this.db.query(
      "SELECT * FROM orders WHERE id = $1", [id.value]
    );
    if (!row) return null;
    return this.toDomain(row);  // Map DB row to Domain Object
  }

  async save(order: Order): Promise {
    const data = this.toPersistence(order);
    await this.db.query(
      `INSERT INTO orders (id, customer_id, status, total)
       VALUES ($1, $2, $3, $4)
       ON CONFLICT (id) DO UPDATE SET status=$3, total=$4`,
      [data.id, data.customerId, data.status, data.total]
    );
  }
}

Domain, Application, Infrastructure Services

// Domain Service — Business Logic ที่ไม่อยู่ใน Entity ตัวใดตัวหนึ่ง
class PricingService {
  calculateDiscount(order: Order, customer: Customer): Money {
    if (customer.isVIP() && order.totalAmount.isGreaterThan(Money.of(5000, "THB"))) {
      return order.totalAmount.multiply(0.1);  // VIP ลด 10%
    }
    return Money.zero();
  }
}

// Application Service — Orchestrate Use Cases
class PlaceOrderUseCase {
  constructor(
    private orderRepo: OrderRepository,
    private customerRepo: CustomerRepository,
    private pricingService: PricingService,
    private eventBus: EventBus,
  ) {}

  async execute(cmd: PlaceOrderCommand): Promise {
    const customer = await this.customerRepo.findById(cmd.customerId);
    if (!customer) throw new NotFoundError("Customer not found");

    const order = Order.create(this.orderRepo.nextId(), customer.id);
    for (const item of cmd.items) {
      order.addItem(item.productId, item.quantity, item.price);
    }

    const discount = this.pricingService.calculateDiscount(order, customer);
    order.applyDiscount(discount);
    order.confirm();

    await this.orderRepo.save(order);

    // Publish Domain Events
    for (const event of order.pullDomainEvents()) {
      await this.eventBus.publish(event);
    }

    return order.id;
  }
}

// Infrastructure Service — Technical Concerns
class StripePaymentGateway implements PaymentGateway {
  async charge(amount: Money, cardToken: string): Promise {
    const result = await stripe.charges.create({
      amount: amount.toCents(),
      currency: amount.currency,
      source: cardToken,
    });
    return new PaymentResult(result.id, result.status);
  }
}

CQRS และ Event Sourcing กับ DDD

CQRS (Command Query Responsibility Segregation)

// Command Side — เขียนผ่าน Domain Model
class PlaceOrderCommand {
  constructor(
    readonly customerId: string,
    readonly items: Array<{ productId: string; quantity: number; price: number }>
  ) {}
}

// Query Side — อ่านจาก Read Model (optimized)
class OrderQueryService {
  async getOrderSummary(orderId: string): Promise {
    // อ่านจาก Denormalized View/Table ที่ออกแบบมาเพื่อ Query
    return this.readDb.query(
      "SELECT * FROM order_summary_view WHERE id = $1", [orderId]
    );
  }

  async getCustomerOrders(customerId: string): Promise {
    return this.readDb.query(
      "SELECT * FROM customer_orders_view WHERE customer_id = $1",
      [customerId]
    );
  }
}

Event Sourcing

// แทนที่จะเก็บ State ปัจจุบัน เก็บ Event ทั้งหมดที่เกิดขึ้น
class Order {
  private events: DomainEvent[] = [];

  // Rebuild State จาก Events
  static fromHistory(events: DomainEvent[]): Order {
    const order = new Order();
    for (const event of events) {
      order.apply(event);
    }
    return order;
  }

  private apply(event: DomainEvent): void {
    if (event instanceof OrderCreated) {
      this.id = event.orderId;
      this.status = OrderStatus.CREATED;
    } else if (event instanceof ItemAdded) {
      this.items.push(new OrderItem(event.productId, event.quantity, event.price));
    } else if (event instanceof OrderConfirmed) {
      this.status = OrderStatus.CONFIRMED;
    }
  }
}

// Event Store
class EventStore {
  async save(aggregateId: string, events: DomainEvent[]): Promise {
    for (const event of events) {
      await this.db.query(
        "INSERT INTO events (aggregate_id, type, data, version) VALUES ($1, $2, $3, $4)",
        [aggregateId, event.constructor.name, JSON.stringify(event), event.version]
      );
    }
  }

  async load(aggregateId: string): Promise {
    const rows = await this.db.query(
      "SELECT * FROM events WHERE aggregate_id = $1 ORDER BY version",
      [aggregateId]
    );
    return rows.map(r => this.deserialize(r));
  }
}

DDD กับ Microservices

Bounded Context สะท้อนไปเป็น Microservice ได้พอดี:

// แต่ละ Bounded Context = 1 Microservice
//
// [Order Service]  ←→  [Payment Service]
//       ↓                      ↓
// [Shipping Service] ←→ [Inventory Service]
//
// สื่อสารผ่าน:
// 1. Domain Events (async, preferred)
// 2. REST/gRPC (sync, เมื่อจำเป็น)
// 3. Anti-Corruption Layer (เมื่อ integrate กับ Legacy)

Anti-Corruption Layer (ACL)

// ป้องกัน Model ของเราจาก External System
class LegacyOrderAdapter implements OrderDataSource {
  constructor(private legacyApi: LegacyAPI) {}

  async getOrder(orderId: string): Promise {
    // Legacy API ส่ง data format แปลกๆ
    const legacyData = await this.legacyApi.fetch_order_v1(orderId);

    // ACL แปลงเป็น Domain Model ของเรา
    return Order.reconstitute({
      id: new OrderId(legacyData.ord_num),
      customerId: new CustomerId(legacyData.cust_code),
      status: this.mapStatus(legacyData.stat_flag),
      items: legacyData.line_items.map(li => new OrderItem(
        new ProductId(li.prod_cd),
        li.qty_ord,
        Money.of(li.unit_prc, "THB")
      )),
    });
  }

  private mapStatus(flag: number): OrderStatus {
    const mapping: Record = {
      0: OrderStatus.CREATED,
      1: OrderStatus.CONFIRMED,
      9: OrderStatus.CANCELLED,
    };
    return mapping[flag] ?? OrderStatus.UNKNOWN;
  }
}

Event Storming Workshop

Event Storming คือ Workshop ที่ช่วยค้นหา Domain Events, Commands และ Aggregate:

  1. Domain Events (สีส้ม): เขียน Event ที่เกิดขึ้นในธุรกิจ เช่น "Order Placed", "Payment Received"
  2. Commands (สีน้ำเงิน): คำสั่งที่ทำให้เกิด Event เช่น "Place Order", "Process Payment"
  3. Aggregates (สีเหลือง): กลุ่มของ Data ที่ต้องเปลี่ยนพร้อมกัน เช่น "Order", "Payment"
  4. Bounded Contexts: วาดเส้นรอบกลุ่มที่เกี่ยวข้องกัน
  5. Policies (สีม่วง): เมื่อเกิด Event A ให้ทำ Command B อัตโนมัติ
เคล็ดลับ: Event Storming ต้องมี Domain Expert ร่วมด้วยเสมอ Developer คนเดียวทำไม่ได้ เพราะจะพลาด Business Rule ที่สำคัญ

DDD กับ Clean/Hexagonal Architecture

// Hexagonal Architecture (Ports & Adapters)
//
// [HTTP Controller] → [Application Service] → [Domain Model]
//                           ↓
//                    [Port Interface]
//                           ↓
//                    [Adapter: Database]
//
// โครงสร้าง Folder:
src/
├── domain/              # Domain Layer (ไม่ depend อะไรเลย)
│   ├── model/
│   │   ├── Order.ts             # Aggregate Root
│   │   ├── OrderItem.ts         # Entity
│   │   └── Money.ts             # Value Object
│   ├── event/
│   │   └── OrderConfirmed.ts    # Domain Event
│   ├── repository/
│   │   └── OrderRepository.ts   # Interface (Port)
│   └── service/
│       └── PricingService.ts    # Domain Service
├── application/         # Application Layer
│   ├── PlaceOrderUseCase.ts
│   └── dto/
│       └── PlaceOrderCommand.ts
├── infrastructure/      # Infrastructure Layer (Adapters)
│   ├── persistence/
│   │   └── PostgresOrderRepository.ts
│   ├── messaging/
│   │   └── RabbitMQEventBus.ts
│   └── external/
│       └── StripePaymentGateway.ts
└── interface/           # Interface Layer
    ├── http/
    │   └── OrderController.ts
    └── graphql/
        └── OrderResolver.ts

DDD vs CRUD — เมื่อไหร่ควรใช้?

ด้านCRUDDDD
Complexityต่ำ-ปานกลางสูง (Complex Domain)
Development Speedเร็ว (เริ่มต้น)ช้ากว่า (เริ่มต้น)
Maintenanceยากเมื่อซับซ้อนขึ้นง่ายกว่าในระยะยาว
Team Sizeทีมเล็กทีมใหญ่หลาย Context
Business Rulesน้อยมากและซับซ้อน
Learning Curveต่ำสูง

ใช้ DDD เมื่อ:

ไม่ต้องใช้ DDD เมื่อ:

Common DDD Mistakes

DDD กับภาษาต่างๆ

# Python กับ DDD (ใช้ dataclass)
from dataclasses import dataclass, field
from typing import List

@dataclass(frozen=True)  # Value Object (immutable)
class Money:
    amount: float
    currency: str = "THB"

    def add(self, other: "Money") -> "Money":
        assert self.currency == other.currency
        return Money(self.amount + other.amount, self.currency)

@dataclass
class Order:  # Aggregate Root
    id: str
    customer_id: str
    items: List["OrderItem"] = field(default_factory=list)
    status: str = "CREATED"
    _events: List = field(default_factory=list, repr=False)

    def add_item(self, product_id: str, qty: int, price: Money):
        if len(self.items) >= 50:
            raise ValueError("Too many items")
        self.items.append(OrderItem(product_id, qty, price))

    def confirm(self):
        if not self.items:
            raise ValueError("Cannot confirm empty order")
        self.status = "CONFIRMED"
        self._events.append(OrderConfirmed(self.id, self.customer_id))

Testing Domain Logic

// Domain Logic ทดสอบง่ายมาก — ไม่ต้อง Mock Database
describe("Order", () => {
  it("should confirm order with items", () => {
    const order = Order.create(OrderId.generate(), customerId);
    order.addItem(productId, 2, Money.of(500, "THB"));

    order.confirm();

    expect(order.status).toBe(OrderStatus.CONFIRMED);
    expect(order.pullDomainEvents()).toContainEqual(
      expect.objectContaining({ type: "OrderConfirmed" })
    );
  });

  it("should reject confirming empty order", () => {
    const order = Order.create(OrderId.generate(), customerId);

    expect(() => order.confirm()).toThrow("Cannot confirm empty order");
  });

  it("should limit items to 50", () => {
    const order = Order.create(OrderId.generate(), customerId);
    for (let i = 0; i < 50; i++) {
      order.addItem(productId, 1, Money.of(100, "THB"));
    }

    expect(() => order.addItem(productId, 1, Money.of(100, "THB")))
      .toThrow("more than 50 items");
  });
});

สรุป

Domain-Driven Design ไม่ใช่แค่ Pattern ในระดับ Code แต่เป็นวิธีคิดในการออกแบบ Software ที่ซับซ้อน หัวใจสำคัญคือ: ให้ Code สะท้อน Business Domain, ใช้ภาษาเดียวกับ Domain Expert, แบ่ง System เป็น Bounded Contexts ที่ชัดเจน

เริ่มต้นจากการทำ Event Storming กับทีม, กำหนด Ubiquitous Language, แล้วค่อยๆ นำ Tactical Patterns (Entity, Value Object, Aggregate, Domain Events) มาใช้ อย่าพยายามใช้ DDD กับทุกอย่าง — ใช้กับส่วนที่ซับซ้อนจริงๆ และปล่อยส่วนที่ง่ายเป็น CRUD ธรรมดา


Back to Blog | iCafe Forex | SiamLanCard | Siam2R