Software Architecture

Unit of Work Pattern từ A đến Z

Admin · Cập nhật 19/07/2026

Hiểu Unit of Work Pattern: gom nhiều thao tác vào một transaction, cách hoạt động, ví dụ với Prisma, best practices và các anti-pattern cần tránh.

Chuyên mục: Software Architecture

Mục lục

  1. Unit of Work là gì?
  2. Vì sao cần?
  3. Cách hoạt động
  4. Transaction và Unit of Work
  5. Ví dụ với Prisma
  6. Khi nào nên dùng
  7. Best Practices
  8. Anti-Patterns
  9. Checklist
  10. FAQ

Unit of Work là gì?

Unit of Work (UoW) là mẫu thiết kế giúp quản lý nhiều thao tác với cơ sở dữ liệu như một đơn vị công việc (Unit). Mọi thay đổi sẽ được commit cùng lúc hoặc rollback toàn bộ nếu xảy ra lỗi.

Bài toán

Giả sử quy trình chuyển tiền:

  1. Trừ tiền tài khoản A.
  2. Cộng tiền tài khoản B.
  3. Ghi lịch sử giao dịch.

Nếu bước 3 thất bại nhưng bước 1 và 2 đã hoàn thành, dữ liệu sẽ không nhất quán.

UoW giải quyết bằng cách thực hiện tất cả trong một transaction.

Kiến trúc

Presentation
    │
Use Case
    │
UnitOfWork
 ├── UserRepository
 ├── WalletRepository
 └── TransactionRepository
    │
 Database Transaction

Ví dụ Interface

export interface UnitOfWork {
  execute<T>(action: () => Promise<T>): Promise<T>;
}

Triển khai với Prisma

export class PrismaUnitOfWork implements UnitOfWork {
  constructor(private prisma: PrismaClient) {}

  async execute<T>(action: (tx: Prisma.TransactionClient) => Promise<T>) {
    return this.prisma.$transaction((tx) => action(tx));
  }
}

Use Case

await unitOfWork.execute(async (tx) => {
  await walletRepository.debit(tx, fromId, amount);
  await walletRepository.credit(tx, toId, amount);
  await historyRepository.create(tx, history);
});

Ưu điểm

  • Đảm bảo tính nhất quán dữ liệu.
  • Hỗ trợ rollback.
  • Dễ quản lý transaction.
  • Phù hợp với nghiệp vụ phức tạp.

Nhược điểm

  • Tăng độ phức tạp.
  • Không cần thiết cho CRUD đơn giản.
  • Transaction dài có thể ảnh hưởng hiệu năng.

Khi nào nên dùng?

  • Chuyển tiền.
  • Đặt hàng.
  • Thanh toán.
  • Cập nhật nhiều bảng liên quan.

So sánh: có và không có Unit of Work

Bảng dưới đây giúp bạn quyết định nhanh khi nào một quy trình cần được gói trong Unit of Work.

Tiêu chí Không dùng UoW (gọi từng thao tác) Dùng Unit of Work
Tính nhất quán Dễ để lại dữ liệu "nửa vời" khi lỗi giữa chừng Toàn bộ commit hoặc rollback cùng lúc
Số bảng bị ảnh hưởng Phù hợp khi chỉ ghi 1 bảng Phù hợp khi ghi nhiều bảng liên quan
Độ phức tạp code Thấp, dễ đọc Cao hơn, thêm một lớp điều phối
Khả năng kiểm thử Khó mô phỏng lỗi giữa chừng Dễ test kịch bản rollback
Hiệu năng Nhanh với thao tác đơn lẻ Có thể giữ khóa (lock) lâu hơn nếu transaction dài

Nguyên tắc chung: nếu một nghiệp vụ ghi vào từ hai bảng trở lên và các thao tác đó phải "cùng thành công hoặc cùng thất bại", hãy dùng Unit of Work. Nếu chỉ là một thao tác CRUD đơn lẻ, transaction là không cần thiết.

Ví dụ thực tế với Next.js + Prisma

Giả sử trong dự án Next.js (App Router) bạn có một Route Handler xử lý việc đặt hàng. Khi tạo đơn hàng, hệ thống cần: (1) tạo bản ghi Order, (2) tạo nhiều OrderItem, và (3) trừ tồn kho trong bảng Product. Cả ba thao tác phải nằm trong cùng một transaction để tránh tình trạng đơn hàng được tạo nhưng tồn kho không bị trừ.

Trước tiên, định nghĩa Repository nhận vào một TransactionClient thay vì PrismaClient trực tiếp. Nhờ vậy Repository có thể tham gia vào transaction do Unit of Work mở:

// lib/repositories/order.repository.ts
import { Prisma } from "@prisma/client";

export class OrderRepository {
  async create(tx: Prisma.TransactionClient, data: Prisma.OrderCreateInput) {
    return tx.order.create({ data });
  }
}

export class ProductRepository {
  async decreaseStock(
    tx: Prisma.TransactionClient,
    productId: string,
    quantity: number,
  ) {
    // updateMany + điều kiện stock >= quantity để tránh bán âm kho
    const result = await tx.product.updateMany({
      where: { id: productId, stock: { gte: quantity } },
      data: { stock: { decrement: quantity } },
    });
    if (result.count === 0) {
      throw new Error("Sản phẩm đã hết hàng");
    }
  }
}

Use Case điều phối các Repository bên trong execute. Nếu bất kỳ bước nào ném lỗi (ví dụ hết hàng), Prisma sẽ tự động rollback toàn bộ:

// lib/use-cases/place-order.ts
export async function placeOrder(input: PlaceOrderInput) {
  return unitOfWork.execute(async (tx) => {
    const order = await orderRepository.create(tx, {
      userId: input.userId,
      total: input.total,
      items: { create: input.items },
    });

    for (const item of input.items) {
      await productRepository.decreaseStock(tx, item.productId, item.quantity);
    }

    return order;
  });
}

Cuối cùng, Route Handler chỉ gọi Use Case và ánh xạ lỗi sang phản hồi HTTP:

// app/api/orders/route.ts
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const body = await request.json();
  try {
    const order = await placeOrder(body);
    return NextResponse.json(order, { status: 201 });
  } catch (error) {
    // Toàn bộ transaction đã rollback, DB vẫn nhất quán
    return NextResponse.json({ message: "Không thể tạo đơn hàng" }, { status: 400 });
  }
}

Điểm mấu chốt: tx được truyền xuyên suốt tất cả Repository. Nếu bạn lỡ gọi prisma.product.update(...) (dùng client gốc thay vì tx) bên trong khối này, thao tác đó sẽ nằm ngoài transaction và không được rollback — đây là một lỗi rất phổ biến.

Best Practices

  • Giữ transaction ngắn.
  • Không gọi API bên ngoài trong transaction.
  • Repository chỉ thao tác dữ liệu.
  • Log lỗi khi rollback.

Anti-Patterns

  • Transaction kéo dài hàng chục giây.
  • Gửi email bên trong transaction.
  • Thực hiện HTTP request trong transaction.

Sai lầm thường gặp

  • Dùng client gốc thay vì transaction client. Bên trong execute, nếu bạn vô tình gọi prisma.model.update(...) thay vì tx.model.update(...), thao tác đó nằm ngoài transaction và sẽ không được rollback khi có lỗi — dữ liệu lại rơi vào trạng thái không nhất quán.
  • Gọi dịch vụ bên ngoài trong transaction. Gửi email, gọi cổng thanh toán hay bắn HTTP request bên trong transaction sẽ giữ khóa DB suốt thời gian chờ mạng. Nếu dịch vụ ngoài chậm hoặc lỗi, transaction bị treo và có thể gây deadlock. Hãy phát các tác vụ này sau khi transaction commit thành công (ví dụ qua Outbox Pattern hoặc hàng đợi).
  • Nhét business logic vào Unit of Work. UoW chỉ nên điều phối transaction, không nên chứa quy tắc nghiệp vụ như tính giá hay kiểm tra quyền. Logic đó thuộc về Domain hoặc Use Case.
  • Transaction quá lớn. Gói hàng trăm thao tác hoặc một vòng lặp dài trong một transaction làm tăng thời gian giữ khóa và nguy cơ xung đột. Hãy chia nhỏ hoặc xử lý theo lô (batch).
  • Nuốt lỗi (swallow) mà không rollback. Bọc từng thao tác trong try/catch rồi bỏ qua lỗi khiến transaction vẫn commit dù có bước thất bại. Hãy để lỗi lan ra ngoài execute để Prisma rollback tự động.
  • Lồng transaction không cần thiết. Mở một $transaction bên trong một $transaction khác thường không hoạt động như kỳ vọng và dễ gây khó hiểu; hãy truyền tx hiện có xuống thay vì mở transaction mới.

Checklist

  • Có cơ chế rollback.
  • Mọi thao tác liên quan cùng transaction.
  • Repository nhận transaction client khi cần.
  • Không chứa business logic trong Unit of Work.

FAQ

Prisma đã có $transaction, còn cần Unit of Work không?

Với dự án nhỏ, $transaction có thể đủ. Trong hệ thống theo Clean Architecture, Unit of Work giúp chuẩn hóa cách quản lý transaction và giảm phụ thuộc trực tiếp vào Prisma.

Unit of Work có thay thế Repository không?

Không. Repository quản lý truy cập dữ liệu, còn Unit of Work điều phối nhiều Repository trong cùng một transaction.

Nên gửi email hay gọi API bên ngoài ở đâu nếu không được đặt trong transaction?

Hãy thực hiện chúng sau khi transaction commit thành công. Một cách phổ biến là ghi một bản ghi "cần gửi email" vào DB trong transaction (Outbox Pattern), rồi một tiến trình nền đọc bản ghi đó và gửi email. Nhờ vậy hành động phụ chỉ xảy ra khi dữ liệu chính đã chắc chắn được lưu.

Transaction bị rollback thì có tự động thử lại (retry) không?

Không, Prisma không tự retry. Với các lỗi có thể xảy ra do tranh chấp (như deadlock hoặc lỗi serialization), bạn nên tự bọc execute trong một vòng lặp retry có giới hạn số lần và độ trễ tăng dần, đồng thời chỉ retry với những mã lỗi phù hợp.

Unit of Work có ảnh hưởng đến hiệu năng không?

Có, ở mức độ nhất định. Transaction giữ khóa trên các dòng dữ liệu cho đến khi commit, nên transaction càng dài thì càng nhiều thao tác khác phải chờ. Cách giảm ảnh hưởng là giữ transaction ngắn gọn, chỉ đưa vào những thao tác thật sự cần tính nguyên tử, và tránh mọi tác vụ I/O mạng bên trong.

Kết luận

Unit of Work là lựa chọn phù hợp khi hệ thống có nhiều thao tác dữ liệu cần đảm bảo tính toàn vẹn. Kết hợp Repository Pattern và Clean Architecture sẽ giúp mã nguồn rõ ràng, dễ kiểm thử và dễ mở rộng.