
Kiến trúc phần mềm
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.
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.
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.
@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[];
}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.
export class Price {
constructor(
public amount: number,
public currency: string = 'VND'
) {}
get formatted(): string {
return `${this.amount.toLocaleString()} ${this.currency}`;
}
}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
├── ProductVariant
│ └── Stock
└── CategoryKhô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 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.
export interface ProductRepository {
findById(id: number): Promise<Product | null>;
save(product: Product): Promise<Product>;
delete(id: number): Promise<void>;
}@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);
}
}// 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.
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);
}// 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.
async findAvailableProducts(): Promise<Product[]> {
return this.repo.find({ where: { stock: MoreThan(0) } });
}// 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.
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);
}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Á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.
Tiếp tục khám phá các chủ đề cùng chuyên mục.

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
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