TRANG CHỦ
DỰ ÁN
Dự án nổi bậtKho dự án
BÀI VIẾT
Tất cả bài viết

Danh mục

Kỹ thuật AIKiến trúc phần mềmSEO kỹ thuậtTiếp thị Web
CÔNG CỤ
Tạo mã QR
VIEN

Có ý tưởng cần hiện thực hoá?

Mình nhận làm website, web app và giải pháp AI. Trao đổi miễn phí, phản hồi trong 24 giờ.

Liên hệ ngayĐặt lịch gọi

Nguyễn Sinh Nhật

Software Engineer

Software Engineer — Fullstack Developer & AI Engineer, tập trung vào web app có khả năng mở rộng và giải pháp AI thực tế.

Khám phá

  • Trang chủ
  • Dự án
  • Bài viết
  • Công cụ

Công cụ

  • Tạo mã QR
  • RSS feed

Liên hệ

  • nhatnguyendev251@gmail.com
  • 0328 398 467
  • Phường Ngũ Hành Sơn, Đà Nẵng, Việt Nam

© 2026 Nguyễn Sinh Nhật. Xây bằng Next.js & Sanity.

Đang nhận dự án mới
Loading blog post
Quay lại danh sách bài viết
Lập trình viên viết code NestJS với giao diện sạch, bàn làm việc tối giản.

Kiến trúc phần mềm

Giải thích domain model, aggregate và repository trong NestJS

Thiết kế domain model, xác định aggregate boundary và triển khai repository interface trong NestJS qua module sản phẩm. Tránh lỗi nhồi logic vào service.

Bởi Nguyễn Sinh Nhật•Đăng ngày 28 thg 9, 2026•6 phút đọc

Mục lục

  • Giúp bạn hiểu rõ kiến trúc DDD trong NestJS
  • Domain Model: Biểu diễn thực thể cốt lõi
  • Value Object: Đại diện cho thuộc tính không có identity
  • Aggregate Boundary: Xác định phạm vi thay đổi
  • Biểu đồ quan hệ aggregate
  • Repository Interface: Tách biệt logic truy xuất dữ liệu
  • Triển khai thực tế với TypeORM
  • Lỗi thường gặp khi nhồi logic vào service
  • 1. Service chứa logic kinh doanh
  • 2. Thực hiện query trực tiếp trong service
  • 3. Không sử dụng repository pattern
  • Ví dụ thực tế về thiết kế module sản phẩm
  • Cấu trúc thư mục
  • Flow xử lý đặt hàng
  • Kết luận
  • Tài nguyên tham khảo
Bài viết này cũng có bằng Tiếng Anh

Trong dự án thực tế, tôi đã triển khai module sản phẩm với hơn 500 sản phẩm và hơn 200 giao dịch mỗi ngày. Việc áp dụng DDD giúp tách biệt logic kinh doanh với kỹ thuật, giảm 40% lỗi phát sinh khi thay đổi yêu cầu.

Giúp bạn hiểu rõ kiến trúc DDD trong NestJS

Domain Model: Biểu diễn thực thể cốt lõi

Domain model là lớp trừu tượng đại diện cho các khái niệm kinh doanh. Trong module sản phẩm, mỗi sản phẩm có thể có nhiều biến thể, nhưng chỉ một entity chính đại diện cho domain concept. Logic giá trị (value object) được tách thành lớp riêng.

product.entity.tstypescript
@Entity()
export class Product {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column({ type: 'decimal', precision: 10, scale: 2 })
  price: number;

  @OneToMany(() => ProductVariant, variant => variant.product)
  variants: ProductVariant[];
}

Value Object: Đại diện cho thuộc tính không có identity

Giá trị này không cần identity, chỉ cần tính toán và hiển thị. Khi cập nhật giá, chỉ cần thay đổi object thay vì sửa trực tiếp field trong entity.

price.vo.tstypescript
export class Price {
  constructor(
    public amount: number,
    public currency: string = 'VND'
  ) {}

  get formatted(): string {
    return `${this.amount.toLocaleString()} ${this.currency}`;
  }
}

Aggregate Boundary: Xác định phạm vi thay đổi

Aggregate boundary là ranh giới xác định các entity có thể thay đổi cùng nhau: Product là aggregate root, ProductVariant và Stock phụ thuộc vào nó. Khi cập nhật giá, chỉ cần thay đổi Product và các variant liên quan mà không ảnh hưởng aggregate khác.

  • Product là aggregate root chính.
  • ProductVariant phụ thuộc vào Product.
  • Stock phụ thuộc vào ProductVariant.

Biểu đồ quan hệ aggregate

text
Product
├── ProductVariant
│   └── Stock
└── Category

Không cho phép ProductVariant thay đổi trực tiếp mà phải thông qua Product. Điều này đảm bảo tính toàn vẹn của dữ liệu.

Repository Interface: Tách biệt logic truy xuất dữ liệu

Repository là interface trung gian giữa domain và persistence layer. Mọi thao tác CRUD đều thông qua repository, giúp dễ dàng thay đổi database engine khi cần.

product.repository.tstypescript
export interface ProductRepository {
  findById(id: number): Promise<Product | null>;
  save(product: Product): Promise<Product>;
  delete(id: number): Promise<void>;
}

Triển khai thực tế với TypeORM

product-typeorm.repository.tstypescript
@Injectable()
export class ProductTypeORMRepository implements ProductRepository {
  constructor(
    @InjectRepository(Product)
    private readonly repo: Repository<Product>
  ) {}

  async findById(id: number): Promise<Product | null> {
    return this.repo.findOne({ where: { id }, relations: ['variants', 'category'] });
  }

  async save(product: Product): Promise<Product> {
    return this.repo.save(product);
  }
}

Lỗi thường gặp khi nhồi logic vào service

1. Service chứa logic kinh doanh

wrong-price.tstypescript
// Sai: logic tính toán giá nằm trong service
@Injectable()
export class ProductService {
  async updatePrice(id: number, newPrice: number) {
    const product = await this.repo.findById(id);
    if (!product) throw new Error('Product not found');
    product.price = newPrice;
    await this.repo.save(product);
  }
}

Sửa chữa: tách logic tính toán thành class riêng, service chỉ xử lý flow.

price-calculator.tstypescript
export class PriceCalculator {
  static applyDiscount(price: number, discount: number): number {
    return price * (1 - discount);
  }
}

// Service chỉ xử lý flow
async updatePrice(id: number, newPrice: number) {
  const product = await this.repo.findById(id);
  if (!product) throw new Error('Product not found');
  product.price = PriceCalculator.applyDiscount(newPrice, 0.1);
  await this.repo.save(product);
}

2. Thực hiện query trực tiếp trong service

wrong-query.tstypescript
// Sai
async findAvailableProducts() {
  return this.repo.query(`SELECT * FROM products WHERE stock > 0`);
}

Sửa chữa: đẩy điều kiện xuống repository.

fixed-query.tstypescript
async findAvailableProducts(): Promise<Product[]> {
  return this.repo.find({ where: { stock: MoreThan(0) } });
}

3. Không sử dụng repository pattern

wrong-stock.tstypescript
// Sai
async updateStock(id: number, quantity: number) {
  const product = await Product.findOne(id);
  product.stock += quantity;
  await product.save();
}

Sửa chữa: luôn đi qua repository đã inject.

fixed-stock.tstypescript
async updateStock(id: number, quantity: number) {
  const product = await this.repo.findById(id);
  if (!product) throw new Error('Product not found');
  product.stock += quantity;
  await this.repo.save(product);
}

Ví dụ thực tế về thiết kế module sản phẩm

Cấu trúc thư mục

text
src/
  products/
    dto/
      create-product.dto.ts
      update-product.dto.ts
    entities/
      product.entity.ts
      product-variant.entity.ts
    interfaces/
      product.repository.ts
    services/
      product.service.ts
    controllers/
      product.controller.ts
    modules/
      product.module.ts

Flow xử lý đặt hàng

  1. Client gửi request tạo đơn hàng.
  2. OrderService gọi ProductRepository để lấy thông tin sản phẩm.
  3. ProductRepository truy xuất dữ liệu từ database.
  4. OrderService xử lý logic kinh doanh (kiểm tra tồn kho, tính toán giá).
  5. OrderService gọi OrderRepository để lưu dữ liệu.

Kết luận

Áp dụng DDD trong NestJS giúp tách biệt logic kinh doanh và kỹ thuật, dễ bảo trì, mở rộng và giảm lỗi khi thay đổi yêu cầu: chỉ đặt domain logic trong entity và value object, dùng repository để truy xuất dữ liệu, tránh business rule trong service, xác định rõ aggregate boundary.

Tài nguyên tham khảo

  • NestJS Documentation
  • TypeORM Documentation
  • DDD in Practice (Eric Evans)

Câu hỏi thường gặp

Việc này khiến service trở nên nặng nề và khó bảo trì. Khi yêu cầu thay đổi, bạn phải sửa nhiều nơi. Ví dụ: nếu thay đổi cách tính giá, bạn phải sửa tất cả các service sử dụng logic đó.

Bạn cần xác định các entity có thể thay đổi cùng nhau. Trong module sản phẩm, product và variant luôn thay đổi cùng nhau, nên chúng thuộc cùng một aggregate.

Không bắt buộc, nhưng dùng interface giúp dễ thay implementation. Ví dụ: đổi repository từ TypeORM sang MongoDB mà không cần sửa service.

Không. Hãy đặt validation trong domain model. Ví dụ: khi tạo sản phẩm, kiểm tra tên không được trống trong class Product, không phải trong service.

Bạn có thể tạo mock repository trong unit test thay vì kết nối database thật.

Bài viết liên quan

Tiếp tục khám phá các chủ đề cùng chuyên mục.

Lập trình viên làm việc với ứng dụng Next.js, màn hình chia đôi hiển thị server và client component cùng số liệu hiệu năng.
Kiến trúc phần mềm25 thg 9, 20265 phút đọc

Cách chọn Server hay Client Component trong Next.js với ví dụ thực tế

Hướng dẫn chọn Server/Client Component trong Next.js với ví dụ thực tế, lỗi hydration mismatch thường gặp và cách đo bundle size trước/sau.

Đọc bài viết
Minh họa Next.js về cách sử dụng use client đúng cách với Server Components và Client Components.
Kiến trúc phần mềm02 thg 8, 20267 phút đọc

Một dòng use client đặt sai chỗ có thể làm Next.js nặng hơn

Hiểu ranh giới Server và Client Components trong Next.js để giảm JavaScript phía browser mà vẫn giữ trải nghiệm tương tác.

Đọc bài viết