Back to blog

Domain Logic Patterns: Transaction Script, Domain Model, Table Module & Service Layer

software-architecturedesign-patternsenterprisebackendsystem-design
Domain Logic Patterns: Transaction Script, Domain Model, Table Module & Service Layer

Mọi enterprise application đều cần trả lời một câu hỏi cơ bản: business logic nằm ở đâu?

Nghe có vẻ đơn giản, nhưng câu trả lời định hình mọi thứ — code dễ bảo trì thế nào, test được thế nào, scale ra sao khi complexity tăng, và developer mới dễ hiểu đến đâu.

Martin Fowler đã xác định bốn patterns để tổ chức domain logic trong cuốn sách Patterns of Enterprise Application Architecture năm 2003. Đây không phải lý thuyết suông — chính là các patterns đang power mọi framework bạn dùng hàng ngày:

  • Express.js route handlers của bạn? Transaction Script.
  • Spring Boot với rich JPA entities? Domain Model.
  • .NET DataSet/DataTable? Table Module.
  • Các @Service classes wrap business logic? Service Layer.

Trong bài này, chúng ta sẽ cover:

✅ Transaction Script — procedural, một script cho mỗi operation
✅ Domain Model — hướng đối tượng, behavior nằm trong domain objects
✅ Table Module — một class cho mỗi table, thao tác trên record sets
✅ Service Layer — thin facade điều phối domain logic
✅ Rich vs anemic domain model — anti-pattern phổ biến nhất
✅ Khi nào dùng pattern nào — decision framework
✅ Cách patterns evolve khi complexity tăng
✅ Code examples thực tế bằng TypeScript và Java


Phổ Domain Logic

Trước khi đi sâu từng pattern, hãy hiểu rằng chúng nằm trên một phổ domain complexity:

Lựa chọn đúng phụ thuộc vào complexity của domain. Hầu hết projects bắt đầu với Transaction Script và evolve sang Domain Model khi business rules phát triển.


Pattern 1: Transaction Script

Nó Là Gì

Transaction Script tổ chức business logic như một procedure cho mỗi business operation. Mỗi operation — đặt hàng, chuyển tiền, duyệt yêu cầu — có function riêng làm mọi thứ: validate input, áp dụng rules, persist data và trả kết quả.

TypeScript Example

// Transaction Script: một function = một business operation
class OrderService {
  constructor(private db: Database) {}
 
  async placeOrder(
    customerId: string,
    items: { productId: string; quantity: number }[]
  ): Promise<Order> {
    // Step 1: Validate customer
    const customer = await this.db.query(
      'SELECT * FROM customers WHERE id = $1',
      [customerId]
    );
    if (!customer) throw new Error('Customer not found');
 
    // Step 2: Check inventory và calculate total
    let total = 0;
    for (const item of items) {
      const product = await this.db.query(
        'SELECT * FROM products WHERE id = $1',
        [item.productId]
      );
      if (!product) throw new Error(`Product ${item.productId} not found`);
      if (product.stock < item.quantity) {
        throw new Error(`Insufficient stock for ${product.name}`);
      }
      total += product.price * item.quantity;
    }
 
    // Step 3: Apply discount rules
    if (total > 100) {
      total *= 0.9; // 10% discount trên $100
    }
    if (customer.tier === 'gold') {
      total *= 0.95; // thêm 5% cho gold customers
    }
 
    // Step 4: Create order và update inventory
    const order = await this.db.query(
      'INSERT INTO orders (customer_id, total, status) VALUES ($1, $2, $3) RETURNING *',
      [customerId, total, 'confirmed']
    );
 
    for (const item of items) {
      await this.db.query(
        'INSERT INTO order_items (order_id, product_id, quantity) VALUES ($1, $2, $3)',
        [order.id, item.productId, item.quantity]
      );
      await this.db.query(
        'UPDATE products SET stock = stock - $1 WHERE id = $2',
        [item.quantity, item.productId]
      );
    }
 
    return order;
  }
 
  async cancelOrder(orderId: string): Promise<void> {
    // Transaction Script khác — procedure riêng
    const order = await this.db.query(
      'SELECT * FROM orders WHERE id = $1',
      [orderId]
    );
    if (!order) throw new Error('Order not found');
    if (order.status === 'shipped') {
      throw new Error('Cannot cancel shipped order');
    }
 
    // Khôi phục inventory
    const items = await this.db.query(
      'SELECT * FROM order_items WHERE order_id = $1',
      [orderId]
    );
    for (const item of items) {
      await this.db.query(
        'UPDATE products SET stock = stock + $1 WHERE id = $2',
        [item.quantity, item.productId]
      );
    }
 
    await this.db.query(
      'UPDATE orders SET status = $1 WHERE id = $2',
      ['cancelled', orderId]
    );
  }
}

Java Example

@Service
@Transactional
public class OrderService {
    private final JdbcTemplate jdbc;
 
    public OrderService(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }
 
    public Order placeOrder(String customerId, List<OrderItemRequest> items) {
        // Step 1: Validate customer
        var customer = jdbc.queryForObject(
            "SELECT * FROM customers WHERE id = ?",
            new CustomerRowMapper(), customerId
        );
 
        // Step 2: Check inventory và calculate total
        BigDecimal total = BigDecimal.ZERO;
        for (var item : items) {
            var product = jdbc.queryForObject(
                "SELECT * FROM products WHERE id = ?",
                new ProductRowMapper(), item.productId()
            );
            if (product.stock() < item.quantity()) {
                throw new InsufficientStockException(product.name());
            }
            total = total.add(product.price().multiply(BigDecimal.valueOf(item.quantity())));
        }
 
        // Step 3: Apply discount rules
        if (total.compareTo(BigDecimal.valueOf(100)) > 0) {
            total = total.multiply(BigDecimal.valueOf(0.9));
        }
        if ("gold".equals(customer.tier())) {
            total = total.multiply(BigDecimal.valueOf(0.95));
        }
 
        // Step 4: Persist
        var orderId = jdbc.queryForObject(
            "INSERT INTO orders (customer_id, total, status) VALUES (?, ?, ?) RETURNING id",
            String.class, customerId, total, "confirmed"
        );
 
        for (var item : items) {
            jdbc.update(
                "INSERT INTO order_items (order_id, product_id, quantity) VALUES (?, ?, ?)",
                orderId, item.productId(), item.quantity()
            );
            jdbc.update(
                "UPDATE products SET stock = stock - ? WHERE id = ?",
                item.quantity(), item.productId()
            );
        }
 
        return new Order(orderId, customerId, total, "confirmed");
    }
}

Khi Nào Dùng Transaction Script

Tình huốngTransaction Script?
Simple CRUD với ít business rules✅ Có
Mỗi operation độc lập, ít rules chung✅ Có
Team nhỏ, rapid prototyping✅ Có
Business rules phức tạp và liên kết chặt❌ Không — dùng Domain Model
Rules thay đổi thường xuyên❌ Không — dùng Domain Model
Domain experts mô tả workflows phức tạp❌ Không — dùng Domain Model

Điểm Mạnh và Yếu

Điểm mạnh:

  • Dễ hiểu — mỗi procedure đọc từ trên xuống
  • Dễ trace — một function, một operation, một flow
  • Overhead thấp — không ORM, không entity mapping, không object graph
  • Phù hợp với raw SQL và functional programming styles

Điểm yếu:

  • Code duplication — discount rules trong placeOrder và calculateQuote bị copy-paste
  • Không có model — concept "order" chỉ tồn tại như database rows, không phải object có behavior
  • Khó test — testing cần database vì logic đan xen với SQL
  • Phình thành "God scripts" — khi rules tích lũy, một function trở thành 500+ dòng

Pattern 2: Domain Model

Nó Là Gì

Domain Model tổ chức business logic như mạng lưới objects liên kết nơi mỗi object đại diện cho concept có ý nghĩa trong domain. Objects có cả data và behavior — chúng không chỉ giữ state, chúng enforce rules.

Đây là pattern mà Domain-Driven Design xây dựng trên. DDD entities, value objects và aggregates là implementations cụ thể của Domain Model pattern.

TypeScript Example

// Value Object: immutable, so sánh bằng value
class Money {
  constructor(
    readonly amount: number,
    readonly currency: string = 'USD'
  ) {
    if (amount < 0) throw new Error('Money cannot be negative');
  }
 
  add(other: Money): Money {
    this.assertSameCurrency(other);
    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
    );
  }
 
  isGreaterThan(other: Money): boolean {
    this.assertSameCurrency(other);
    return this.amount > other.amount;
  }
 
  private assertSameCurrency(other: Money): void {
    if (this.currency !== other.currency) {
      throw new Error(`Cannot combine ${this.currency} with ${other.currency}`);
    }
  }
}
 
// Entity: xác định bằng ID, có behavior
class OrderItem {
  constructor(
    readonly product: Product,
    private _quantity: number
  ) {
    if (_quantity <= 0) throw new Error('Quantity must be positive');
    if (_quantity > product.stock) {
      throw new Error(`Insufficient stock for ${product.name}`);
    }
  }
 
  get quantity(): number { return this._quantity; }
 
  get lineTotal(): Money {
    return this.product.price.multiply(this._quantity);
  }
}
 
// Aggregate root: entry point cho mọi order operations
class Order {
  private _items: OrderItem[] = [];
  private _status: OrderStatus = 'draft';
 
  constructor(
    readonly id: string,
    readonly customer: Customer
  ) {}
 
  get status(): OrderStatus { return this._status; }
  get items(): ReadonlyArray<OrderItem> { return this._items; }
 
  // Business logic NẰM Ở ĐÂY, không phải trong service
  addItem(product: Product, quantity: number): void {
    if (this._status !== 'draft') {
      throw new Error('Cannot modify a confirmed order');
    }
 
    const existing = this._items.find(i => i.product.id === product.id);
    if (existing) {
      throw new Error(`Product ${product.name} already in order — modify quantity instead`);
    }
 
    this._items.push(new OrderItem(product, quantity));
  }
 
  calculateTotal(): Money {
    const subtotal = this._items.reduce(
      (sum, item) => sum.add(item.lineTotal),
      new Money(0)
    );
 
    return this.applyDiscounts(subtotal);
  }
 
  confirm(): void {
    if (this._status !== 'draft') {
      throw new Error(`Cannot confirm order in ${this._status} status`);
    }
    if (this._items.length === 0) {
      throw new Error('Cannot confirm empty order');
    }
 
    this._status = 'confirmed';
  }
 
  cancel(): void {
    if (this._status === 'shipped') {
      throw new Error('Cannot cancel shipped order');
    }
    this._status = 'cancelled';
  }
 
  // Private: discount rules được encapsulate trong domain
  private applyDiscounts(subtotal: Money): Money {
    let total = subtotal;
 
    if (total.isGreaterThan(new Money(100))) {
      total = total.multiply(0.9);
    }
 
    if (this.customer.tier === 'gold') {
      total = total.multiply(0.95);
    }
 
    return total;
  }
}
 
type OrderStatus = 'draft' | 'confirmed' | 'shipped' | 'cancelled';

Java Example

// Value Object
public record Money(BigDecimal amount, String currency) {
    public Money {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Money cannot be negative");
        }
    }
 
    public Money add(Money other) {
        assertSameCurrency(other);
        return new Money(amount.add(other.amount), currency);
    }
 
    public Money multiply(double factor) {
        return new Money(
            amount.multiply(BigDecimal.valueOf(factor))
                   .setScale(2, RoundingMode.HALF_UP),
            currency
        );
    }
 
    public boolean isGreaterThan(Money other) {
        assertSameCurrency(other);
        return amount.compareTo(other.amount) > 0;
    }
 
    private void assertSameCurrency(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException(
                "Cannot combine " + currency + " with " + other.currency
            );
        }
    }
}
 
// Aggregate root
public class Order {
    private final String id;
    private final Customer customer;
    private final List<OrderItem> items = new ArrayList<>();
    private OrderStatus status = OrderStatus.DRAFT;
 
    public Order(String id, Customer customer) {
        this.id = id;
        this.customer = customer;
    }
 
    // Business logic nằm trong entity
    public void addItem(Product product, int quantity) {
        if (status != OrderStatus.DRAFT) {
            throw new IllegalStateException("Cannot modify a confirmed order");
        }
 
        boolean exists = items.stream()
            .anyMatch(i -> i.getProduct().getId().equals(product.getId()));
        if (exists) {
            throw new IllegalArgumentException("Product already in order");
        }
 
        items.add(new OrderItem(product, quantity));
    }
 
    public Money calculateTotal() {
        Money subtotal = items.stream()
            .map(OrderItem::lineTotal)
            .reduce(new Money(BigDecimal.ZERO, "USD"), Money::add);
 
        return applyDiscounts(subtotal);
    }
 
    public void confirm() {
        if (status != OrderStatus.DRAFT) {
            throw new IllegalStateException("Cannot confirm order in " + status);
        }
        if (items.isEmpty()) {
            throw new IllegalStateException("Cannot confirm empty order");
        }
        this.status = OrderStatus.CONFIRMED;
    }
 
    public void cancel() {
        if (status == OrderStatus.SHIPPED) {
            throw new IllegalStateException("Cannot cancel shipped order");
        }
        this.status = OrderStatus.CANCELLED;
    }
 
    private Money applyDiscounts(Money subtotal) {
        Money total = subtotal;
        if (total.isGreaterThan(new Money(BigDecimal.valueOf(100), "USD"))) {
            total = total.multiply(0.9);
        }
        if (customer.getTier() == CustomerTier.GOLD) {
            total = total.multiply(0.95);
        }
        return total;
    }
}

Anti-Pattern Anemic Domain Model

Sai lầm phổ biến nhất khi cố dùng Domain Model là tạo ra anemic domain model — objects có data nhưng không behavior. Toàn bộ logic nằm trong service classes, và "domain objects" chỉ là data bags.

// ❌ ANEMIC: domain object chỉ là data bag
class Order {
  id: string;
  customerId: string;
  items: OrderItem[];
  total: number;
  status: string;
  // Không behavior — chỉ getters và setters
}
 
// ❌ Toàn bộ logic leak vào service
class OrderService {
  placeOrder(order: Order, items: OrderItem[]): void {
    // Business rules ở đây, không phải trong Order
    order.items = items;
    order.total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
    if (order.total > 100) order.total *= 0.9;
    order.status = 'confirmed';
    this.repository.save(order);
  }
}
// ✅ RICH: domain object sở hữu behavior
class Order {
  private _items: OrderItem[] = [];
  private _status: OrderStatus = 'draft';
 
  // Logic NẰM Ở ĐÂY, trong domain object
  addItem(product: Product, quantity: number): void {
    // validates, enforce rules
  }
 
  calculateTotal(): Money {
    // encapsulate discount logic
  }
 
  confirm(): void {
    // guard state transitions
  }
}
 
// Service mỏng — chỉ điều phối
class OrderApplicationService {
  async placeOrder(customerId: string, itemRequests: ItemRequest[]): Promise<OrderId> {
    const customer = await this.customerRepo.findById(customerId);
    const order = new Order(generateId(), customer);
 
    for (const req of itemRequests) {
      const product = await this.productRepo.findById(req.productId);
      order.addItem(product, req.quantity); // Domain object enforce rules
    }
 
    order.confirm(); // Domain object guard state transition
    await this.orderRepo.save(order);
    return order.id;
  }
}

Martin Fowler về Anemic Domain Model: "The fundamental horror of this anti-pattern is that it's so contrary to the basic idea of object-oriented design; which is to combine data and process together."

Khi Nào Dùng Domain Model

Tình huốngDomain Model?
Complex business rules tương tác lẫn nhau✅ Có
Rules thay đổi thường xuyên✅ Có
Nhiều đường đi trong business process✅ Có
Domain experts mô tả workflows phức tạp✅ Có
Simple CRUD với ít rules❌ Không — Transaction Script đơn giản hơn
Data-centric reporting application❌ Không — Table Module có thể phù hợp hơn

Pattern 3: Table Module

Nó Là Gì

Table Module tổ chức business logic như một class cho mỗi database table, nơi class thao tác trên collection of rows thay vì single entity instance. Khác với Domain Model (một object cho mỗi row), Table Module cho bạn một object cho toàn bộ table.

Pattern này được phổ biến bởi .NET DataSet/DataTable và là trung gian giữa Transaction Script và Domain Model.

TypeScript Example

// Table Module: một class cho mỗi table, thao tác trên record sets
class OrderModule {
  constructor(private dataSet: Map<string, Record<string, unknown>[]>) {}
 
  calculateTotal(orderId: string): number {
    const orderItems = this.dataSet.get('order_items')
      ?.filter(row => row.orderId === orderId) ?? [];
 
    const products = this.dataSet.get('products') ?? [];
 
    let total = 0;
    for (const item of orderItems) {
      const product = products.find(p => p.id === item.productId);
      if (product) {
        total += (product.price as number) * (item.quantity as number);
      }
    }
 
    return total;
  }
 
  applyDiscount(orderId: string, customerTier: string): number {
    let total = this.calculateTotal(orderId);
 
    if (total > 100) total *= 0.9;
    if (customerTier === 'gold') total *= 0.95;
 
    const orders = this.dataSet.get('orders') ?? [];
    const order = orders.find(o => o.id === orderId);
    if (order) order.total = total;
 
    return total;
  }
 
  getOrdersByStatus(status: string): Record<string, unknown>[] {
    return (this.dataSet.get('orders') ?? [])
      .filter(row => row.status === status);
  }
}

Khi Nào Dùng Table Module

Tình huốngTable Module?
Data-centric app với record set operations✅ Có
Môi trường .NET với DataSet/DataTable✅ Có
Reporting và batch processing✅ Có
Business logic vừa phải — hơn CRUD, ít hơn complex domain✅ Có
Complex domain với rich entity behavior❌ Không — dùng Domain Model
Cần individual object identity và lifecycle❌ Không — dùng Domain Model

Table Module vs Domain Model

Khía cạnhTable ModuleDomain Model
GranularityMột object cho mỗi tableMột object cho mỗi row/entity
IdentityLàm việc với record setsMỗi object có identity
BehaviorMethods thao tác trên collectionsMethods thao tác trên single instance
ORM fitDataSets, raw queriesJPA, Hibernate, Prisma
ComplexityTrung bìnhCao
TestingTest với in-memory datasetsTest individual objects

Trong thực tế, Table Module ít phổ biến ngày nay. Hầu hết framework hiện đại (Spring, Rails, Django, NestJS) đẩy bạn theo hướng Transaction Script (simple services với queries) hoặc Domain Model (entities với behavior). Table Module chủ yếu sống trong .NET DataSet world và legacy applications.


Pattern 4: Service Layer

Nó Là Gì

Service Layer định nghĩa boundary của application — tập hợp operations khả dụng và coordination logic cho mỗi operation. Nó là thin facade nằm giữa presentation layer và domain logic.

Keyword quan trọng là thin. Service Layer điều phối nhưng không chứa business rules. Nó orchestrate domain objects, handle transactions và dispatch notifications — nhưng rules thực sự nằm trong domain.

Service Layer + Domain Model (Kết Hợp Phổ Biến Nhất)

// Application Service: điều phối, không chứa rules
class OrderApplicationService {
  constructor(
    private orderRepo: OrderRepository,
    private customerRepo: CustomerRepository,
    private productRepo: ProductRepository,
    private eventBus: EventBus,
    private emailService: EmailService
  ) {}
 
  async placeOrder(command: PlaceOrderCommand): Promise<string> {
    // 1. Load domain objects
    const customer = await this.customerRepo.findById(command.customerId);
    if (!customer) throw new NotFoundException('Customer not found');
 
    // 2. Tạo và populate aggregate
    const order = new Order(generateId(), customer);
 
    for (const item of command.items) {
      const product = await this.productRepo.findById(item.productId);
      if (!product) throw new NotFoundException(`Product ${item.productId} not found`);
      order.addItem(product, item.quantity); // Domain object validates
    }
 
    // 3. Domain object enforce rules
    order.confirm();
 
    // 4. Persist (infrastructure concern)
    await this.orderRepo.save(order);
 
    // 5. Side effects (infrastructure concern)
    await this.eventBus.publish(new OrderConfirmedEvent(order.id));
    await this.emailService.sendOrderConfirmation(customer.email, order);
 
    return order.id;
  }
 
  async cancelOrder(orderId: string): Promise<void> {
    const order = await this.orderRepo.findById(orderId);
    if (!order) throw new NotFoundException('Order not found');
 
    order.cancel(); // Domain object guard transition này
 
    await this.orderRepo.save(order);
    await this.eventBus.publish(new OrderCancelledEvent(orderId));
  }
}

Service Layer + Transaction Script

Khi domain đơn giản, Service Layer và Transaction Script thường merge làm một:

// Cho simple domains, Service Layer CHÍNH LÀ Transaction Script
class UserService {
  constructor(
    private userRepo: UserRepository,
    private hasher: PasswordHasher,
    private emailService: EmailService
  ) {}
 
  async register(email: string, password: string): Promise<User> {
    // Đơn giản — không cần rich User domain object
    const existing = await this.userRepo.findByEmail(email);
    if (existing) throw new ConflictError('Email already registered');
 
    const hashedPassword = await this.hasher.hash(password);
    const user = await this.userRepo.create({ email, hashedPassword });
 
    await this.emailService.sendWelcome(email);
    return user;
  }
}

Java Example: Spring Boot Service Layer

@Service
@Transactional
public class OrderApplicationService {
    private final OrderRepository orderRepo;
    private final CustomerRepository customerRepo;
    private final ProductRepository productRepo;
    private final ApplicationEventPublisher eventPublisher;
 
    public OrderApplicationService(
            OrderRepository orderRepo,
            CustomerRepository customerRepo,
            ProductRepository productRepo,
            ApplicationEventPublisher eventPublisher
    ) {
        this.orderRepo = orderRepo;
        this.customerRepo = customerRepo;
        this.productRepo = productRepo;
        this.eventPublisher = eventPublisher;
    }
 
    public String placeOrder(PlaceOrderCommand command) {
        var customer = customerRepo.findById(command.customerId())
            .orElseThrow(() -> new NotFoundException("Customer not found"));
 
        var order = new Order(UUID.randomUUID().toString(), customer);
 
        for (var item : command.items()) {
            var product = productRepo.findById(item.productId())
                .orElseThrow(() -> new NotFoundException("Product not found"));
            order.addItem(product, item.quantity()); // Domain validates
        }
 
        order.confirm(); // Domain guard state transition
 
        orderRepo.save(order);
        eventPublisher.publishEvent(new OrderConfirmedEvent(order.getId()));
 
        return order.getId();
    }
}

Application Service vs Domain Service

Một nhầm lẫn thường gặp: khác biệt giữa application service (Service Layer) và domain service (phần của Domain Model)?

Khía cạnhApplication ServiceDomain Service
Mục đíchĐiều phối workflowEncapsulate domain logic không thuộc về entity nào
Biết vềRepositories, events, email, transactionsChỉ domain objects
Ví dụplaceOrder() — load entities, gọi domain methods, save, publish eventsPricingService.calculateDiscount(order, promotions) — pure domain logic
DependenciesInfrastructure + domainChỉ domain
TestabilityCần mocks cho infrastructurePure unit tests
// Domain Service: pure domain logic, không infrastructure
class PricingService {
  calculateDiscount(order: Order, promotions: Promotion[]): Money {
    let discount = new Money(0);
 
    for (const promo of promotions) {
      if (promo.appliesTo(order)) {
        discount = discount.add(promo.calculateAmount(order));
      }
    }
 
    return discount;
  }
}
 
// Application Service: điều phối infrastructure + domain
class OrderApplicationService {
  async placeOrder(command: PlaceOrderCommand): Promise<string> {
    const order = /* ... build order ... */;
    const promos = await this.promoRepo.findActive();
 
    // Delegate complex domain logic cho domain service
    const discount = this.pricingService.calculateDiscount(order, promos);
    order.applyDiscount(discount);
 
    order.confirm();
    await this.orderRepo.save(order);
    return order.id;
  }
}

Chọn Đúng Pattern

Decision Framework

Bảng So Sánh

Khía cạnhTransaction ScriptTable ModuleDomain ModelService Layer
ComplexityThấpTrung bìnhCaoThêm coordination
Phù hợpSimple opsData-centricComplex domainsTất cả (facade)
Rủi ro duplicationCaoTrung bìnhThấpN/A
TestabilityKhó (cần DB)Trung bìnhCao (pure objects)Trung bình (mock deps)
Learning curveTối thiểuThấpDốcThấp
OOP alignmentProceduralMixedFull OOPProcedural facade
FrameworkExpress handlers.NET DataSetSpring/JPA, DDD@Service, NestJS

Framework Mapping Thực Tế

FrameworkDefault PatternLý do
Express.jsTransaction ScriptRoute handlers như procedures, không built-in ORM
RailsActive Record + Transaction ScriptConvention ưu tiên procedural logic đơn giản
DjangoActive Record + Transaction ScriptTương tự Rails — views như scripts, models như data
Spring BootService Layer + Domain ModelThiết kế cho complex enterprise domains
NestJSService Layer + Transaction ScriptServices như providers, nhưng thường procedural
ASP.NET CoreService Layer + Domain ModelEnterprise-oriented với EF Core
LaravelActive Record + Transaction ScriptEloquent models với procedural service classes

Pattern Evolution: Cách Projects Phát Triển

Hầu hết projects không giữ mãi một pattern. Khi complexity tăng, domain logic pattern evolve:

Stage 1: Transaction Script (Ngày 1)

// Đơn giản, nhanh, hoạt động. Ship thôi.
app.post('/orders', async (req, res) => {
  const { customerId, items } = req.body;
  // ... mọi logic trong một function
  const order = await db.query('INSERT INTO orders...');
  res.json(order);
});

Stage 2: Extract Service Layer (Tháng 3)

// Logic chuyển từ controller vào service
class OrderService {
  async placeOrder(customerId: string, items: ItemRequest[]): Promise<Order> {
    // Cùng Transaction Script, nhưng giờ reusable và testable
  }
}
 
app.post('/orders', async (req, res) => {
  const order = await orderService.placeOrder(req.body.customerId, req.body.items);
  res.json(order);
});

Stage 3: Introduce Domain Model (Tháng 9)

// Business rules phức tạp dần — extract vào domain objects
class Order {
  addItem(product: Product, quantity: number): void { /* validates */ }
  calculateTotal(): Money { /* complex discounts */ }
  confirm(): void { /* state machine */ }
}
 
// Service Layer trở nên mỏng
class OrderApplicationService {
  async placeOrder(command: PlaceOrderCommand): Promise<string> {
    const order = new Order(id, customer);
    for (const item of command.items) {
      order.addItem(product, item.quantity);
    }
    order.confirm();
    await this.orderRepo.save(order);
    return order.id;
  }
}

Stage 4: Full DDD (Năm 2+)

// Aggregates, value objects, domain events, bounded contexts
class Order { /* aggregate root với full invariant enforcement */ }
class Money { /* value object với currency math */ }
class OrderConfirmedEvent { /* domain event cho cross-context communication */ }

Insight chính: bạn không cần bắt đầu với Domain Model. Bắt đầu với Transaction Script, và refactor sang Domain Model khi complexity đòi hỏi. Đây là cái Martin Fowler gọi là evolutionary design.


Sai Lầm Thường Gặp

Sai Lầm 1: Premature Domain Model

❌ Build rich Domain Model cho simple CRUD app
❌ Tạo Value Objects và Aggregates cho basic data storage
❌ Áp DDD patterns cho prototype có thể không sống sót
 
✅ Bắt đầu với Transaction Script
✅ Refactor sang Domain Model khi rules phát triển
✅ Chỉ dùng DDD cho core subdomains

Sai Lầm 2: God Transaction Script

❌ Một function 800 dòng handle toàn bộ workflow
❌ Không extract shared logic
❌ If-else chains lồng sâu
 
✅ Extract helper functions cho shared rules
✅ Giữ mỗi script focus vào một operation
✅ Khi scripts share quá nhiều logic, xem xét Domain Model

Sai Lầm 3: Anemic Domain Model

❌ Domain objects chỉ có getters/setters
❌ Toàn bộ behavior trong service classes
❌ "Domain Model" thực ra là Transaction Script với thêm classes
 
✅ Business rules thuộc về domain objects
✅ Services điều phối, không chứa rules
✅ Nếu không test domain logic mà không cần infrastructure → nó anemic

Sai Lầm 4: Fat Service Layer

❌ Service Layer chứa toàn bộ business logic
❌ Domain objects chỉ là data transfer objects
❌ Services có 50+ methods và 2000+ dòng
 
✅ Service Layer phải mỏng — điều phối, delegate, orchestrate
✅ Domain logic nằm trong domain objects hoặc domain services
✅ Nếu service layer béo, extract rules vào domain

Bài Tập Thực Hành

Bài Tập 1: Nhận Diện Pattern

Nhìn vào project bạn đang làm. Cho mỗi service class, xác định:

  • Nó là Transaction Script? (procedural, mọi logic trong method)
  • Nó là Domain Model? (delegate cho rich domain objects)
  • Nó là anemic Domain Model? (có "domain" objects, nhưng logic trong services)

Bài Tập 2: Refactor Transaction Script thành Domain Model

Lấy Transaction Script này và refactor thành Domain Model với thin Service Layer:

// Refactor cái này thành rich Account domain model
async function transferMoney(fromAccountId: string, toAccountId: string, amount: number) {
  const from = await db.query('SELECT * FROM accounts WHERE id = $1', [fromAccountId]);
  const to = await db.query('SELECT * FROM accounts WHERE id = $1', [toAccountId]);
 
  if (from.balance < amount) throw new Error('Insufficient funds');
  if (from.status !== 'active') throw new Error('Source account is not active');
  if (to.status !== 'active') throw new Error('Target account is not active');
  if (amount <= 0) throw new Error('Amount must be positive');
  if (amount > 10000) throw new Error('Transfer limit exceeded');
 
  await db.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromAccountId]);
  await db.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toAccountId]);
  await db.query('INSERT INTO transfers (from_id, to_id, amount) VALUES ($1, $2, $3)',
    [fromAccountId, toAccountId, amount]);
}

Bài Tập 3: Thiết Kế Service Layer

Cho e-commerce system, thiết kế Service Layer interface:

  • OrderService expose operations nào?
  • ProductService expose operations nào?
  • Inventory check logic nằm ở đâu — trong service hay domain?

Bài Tập 4: So Sánh Frameworks

Build cùng tính năng đặt hàng bằng:

  1. Express.js (Transaction Script style)
  2. Spring Boot (Domain Model + Service Layer style)

So sánh: số dòng code, testability, rules nằm ở đâu, dễ thêm discount rule mới không.


Tóm Tắt và Điểm Chính

✅ Transaction Script — một procedure cho mỗi operation. Đơn giản, nhanh, phù hợp CRUD. Xuống cấp khi complexity tăng.
✅ Domain Model — business logic trong domain objects. Tốt nhất cho complex rules. Cần kỷ luật để tránh anemic models.
✅ Table Module — một class cho mỗi table thao tác trên record sets. Trung gian, chủ yếu .NET. Ít phổ biến ngày nay.
✅ Service Layer — thin facade điều phối domain logic. Hầu như luôn dùng kèm Transaction Script hoặc Domain Model.
✅ Kết hợp phổ biến nhất trong enterprise apps: Service Layer + Domain Model
✅ Anti-pattern phổ biến nhất: Anemic Domain Model (cấu trúc Domain Model với behavior Transaction Script)
✅ Bắt đầu đơn giản (Transaction Script), evolve khi complexity đòi hỏi (Domain Model)
✅ Framework đã chọn default pattern — hiểu và làm việc cùng nó, không chống lại


Tiếp Theo?

Bây giờ bạn biết cách tổ chức business logic, câu hỏi tiếp là: persist nó thế nào? Trong Bài #3: Data Source Patterns, chúng ta sẽ cover Active Record, Data Mapper, Table Data Gateway và Row Data Gateway — patterns kết nối domain với database.


Bài Viết Liên Quan


Series: Patterns of Enterprise Application Architecture
Tiếp theo: Bài #3 — Data Source Patterns (Active Record, Data Mapper, Table Data Gateway, Row Data Gateway)

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.