CQRS — tách đường ghi khỏi đường đọc và cái giá nhất quán
CQRS tách mô hình ghi (command) khỏi mô hình đọc (query): dựng nhiều read model tối ưu cho từng truy vấn. Vì sao tách, và cái giá của nhất quán cuối cùng.
TL;DR: CQRS (Command Query Responsibility Segregation) tách mô hình ghi (write model / command) ra khỏi mô hình đọc (read model / query). Thay vì một bảng DB vừa nhận INSERT/UPDATE vừa phục vụ SELECT phức tạp, bạn dựng nhiều materialized view tối ưu riêng cho từng truy vấn — mỗi view cập nhật bất đồng bộ khi có sự kiện mới. Điểm then chốt: read model trễ sau write model (eventual consistency), lag thông thường ~50ms nhưng có thể lên vài giây khi tải cao. Martin Fowler cảnh báo CQRS thêm complexity đáng kể — chỉ xứng đáng khi read/write load thật sự khác nhau và hệ thống đủ lớn để hưởng lợi.
Hệ thống thanh toán của bạn xử lý 50.000 giao dịch mỗi phút vào giờ cao điểm. Bảng transactions vừa phải nhận INSERT liên tục, vừa phải phục vụ truy vấn tổng hợp phức tạp: "số dư tài khoản hiện tại", "lịch sử 30 ngày gần nhất có phân trang", "bảng điều khiển analytic theo khu vực", "báo cáo thuế 12 tháng". Mỗi truy vấn cần index khác nhau, schema khác nhau — và chúng đang block lẫn nhau trên cùng một bảng.
Bài này giải thích CQRS giải quyết bài toán đó bằng cách nào, so sánh rõ với CRUD truyền thống, và buộc bạn lập luận khi nào chấp nhận nhất quán cuối cùng là đáng đổi.
1. Analogy — nhà hàng tách cửa đặt món khỏi bảng trạng thái
Hình dung nhà hàng lớn vào giờ cao điểm.
Mô hình cũ (CRUD): cùng một cửa duy nhất cho cả đặt món, bếp trả món, thu ngân, và thực khách hỏi trạng thái đơn. Khi khách xếp hàng đặt món, đội bưng bê cũng phải chờ ở cùng cửa — hai luồng block lẫn nhau.
Mô hình mới (CQRS): bếp có hai cửa tách biệt: cửa đặt món (command — chỉ nhận lệnh ghi, không trả dữ liệu) và bảng trạng thái điện tử (query — chỉ phục vụ đọc, cập nhật bất đồng bộ từ bếp). Thực khách xem bảng mà không phải hỏi bếp trực tiếp.
| Nhà hàng | CQRS |
|---|---|
| Cửa đặt món (chỉ nhận order) | Command / Write model |
| Bảng hiển thị trạng thái | Read model / materialized view |
| Bảng cập nhật sau khi bếp nấu xong | Eventual consistency |
| Bảng chưa phản ánh order vừa đặt | Read-after-write stale |
| Bảng menu khác bảng trạng thái đơn | Nhiều read model cho nhiều truy vấn |
Command = cửa đặt món (ghi, không trả dữ liệu). Query = bảng trạng thái (đọc, không thay đổi gì). Hai cửa hoàn toàn tách nhau — bếp tự cập nhật bảng bất đồng bộ sau khi nhận order.
2. CQRS là gì — tách command khỏi query
CQRS (Command Query Responsibility Segregation) là nguyên tắc phân tách trách nhiệm: mọi thao tác hệ thống thuộc về đúng một trong hai loại:
- Command (lệnh ghi): thay đổi trạng thái, không trả dữ liệu. Ví dụ:
PlaceOrder,TransferFunds,UpdateProfile. Command chạy qua write model — thường là một aggregate lưu trong DB chuẩn hoá, hoặc event store nếu kết hợp với event sourcing. - Query (truy vấn đọc): trả dữ liệu, không thay đổi trạng thái. Query đọc từ read model — một hoặc nhiều materialized view được xây riêng cho từng trường hợp dùng.
Materialized view là kết quả tính sẵn của một truy vấn phức tạp, lưu trữ dưới dạng bảng hoặc document riêng. Ví dụ: account_balance_view tính sẵn số dư từ transactions; order_history_view denormalize bảng orders cùng products thành một document duy nhất để GET và phân trang nhanh mà không cần JOIN.
Khi write model nhận command và thay đổi state, nó phát ra một domain event (ví dụ MoneyTransferred, OrderPlaced). Read model lắng nghe event đó và cập nhật materialized view bất đồng bộ.

Điều quan trọng: read model không đọc trực tiếp từ write DB. Nó chỉ đọc từ materialized view — đã được xây sẵn tối ưu cho truy vấn đó. Khi cần kiểu truy vấn mới (ví dụ thêm trang analytic theo khu vực), bạn thêm một projection mới mà không chạm write model.
3. So sánh CQRS với CRUD truyền thống
Trước khi đọc bảng so sánh bên dưới, thử điền vào chỗ trống: CRUD truyền thống giải quyết truy vấn đọc phức tạp bằng cách nào? Điều gì xảy ra khi read/write load tăng mạnh trên cùng một bảng? Ghi câu trả lời ngắn rồi đối chiếu.
| Tiêu chí | CRUD truyền thống | CQRS |
|---|---|---|
| Mô hình lưu trữ | Một schema phục vụ cả đọc và ghi | Write model riêng + nhiều read model riêng |
| Đọc phức tạp | JOIN nhiều bảng, index phức tạp | Materialized view xây sẵn cho từng truy vấn |
| Nhất quán | Strong consistency — đọc ngay sau ghi thấy giá trị mới | Eventual consistency — read model trễ sau write model |
| Scale đọc | Replica DB, thêm index, cache | Mỗi read model scale độc lập, chọn store phù hợp |
| Scale ghi | Vertical scale, partition | Write model scale độc lập |
| Tối ưu đọc vs ghi | Index tốt cho đọc làm chậm ghi (và ngược lại) | Mỗi phía tối ưu riêng — không trade-off lẫn nhau |
| Độ phức tạp | Thấp — một source of truth, không cần đồng bộ | Cao — projection, eventual consistency, nhiều data store |
| Debug và trace | Đọc bảng gốc là đủ | Phải trace event → projection → view |
| Phù hợp nhất | Hầu hết ứng dụng | Khi read/write load thật sự khác nhau ở quy mô lớn |
CRUD truyền thống giải quyết truy vấn phức tạp bằng index, DB view (không materialized), và replica. Khi write tăng, bảng bị lock; khi read tăng, cần thêm index — nhưng index làm chậm write. Hai workload đối nghịch nhau trên cùng schema.
CQRS cho phép tối ưu mỗi phía độc lập: write model chọn schema chuẩn hoá cho integrity; read model chọn bất kỳ store nào (PostgreSQL, Elasticsearch, Redis) phù hợp nhất cho truy vấn đó. Nhưng đổi lại là độ phức tạp: phải duy trì projection pipeline, xử lý event mất mát, và — quan trọng nhất — chấp nhận eventual consistency.
4. Eventual consistency — cái giá của việc tách
Eventual consistency (nhất quán cuối cùng): sau khi write model chấp nhận command, read model sẽ phản ánh thay đổi đó — nhưng không phải ngay lập tức. Trong khoảng lag đó, read model chứa dữ liệu cũ hơn write model.
Số liệu thực tế từ hệ thống production:
| Tình huống | Lag Write → Read |
|---|---|
| Tải bình thường | ~50ms |
| Cao điểm (flash sale) — trung bình | ~300ms |
| Cao điểm — tail p99 | vượt 2 giây |
| Khi deploy hoặc restart projection service | ~5 giây |
| Khi projection service gặp sự cố | vài phút |
Read-after-write stale là hệ quả trực tiếp: người dùng vừa thực hiện thao tác ghi, ngay sau đó đọc lại — thấy dữ liệu cũ hoặc nhận 404 (entity chưa propagate vào read model).
Ví dụ kinh điển: POST /articles tạo bài viết thành công (201 Created), nhưng GET /articles/123 ngay sau đó trả 404 — vì projection chưa cập nhật articles_view. Với người dùng, cảm giác là "hệ thống mất bài vừa đăng".
Bạn đang xây hệ thống chuyển tiền: POST /transfer xử lý thành công, rồi client gọi ngay GET /balance. Nếu lag giữa write model và read model là 5 giây — client sẽ thấy gì? Hành vi tiếp theo của họ có thể là gì? Ghi ra câu trả lời.
Incident thực tế trong hệ thống payment: lag 5 giây giữa write model và read model → sau POST /transfer thành công, client gọi GET /balance nhận về số dư cũ (chưa trừ tiền) → client cho rằng lệnh chuyển tiền thất bại → gửi lại POST /transfer lần hai → double-submit → chargebacks và kiện toán.
Đáp án cho Tự đoán: client thấy số dư chưa thay đổi, nghĩ lệnh thất bại, gửi lại → double-submit. Đây là lý do hệ thống payment-critical cần SLO nghiêm ngặt: p99 read model dưới 200ms; vượt ngưỡng đó phải xử lý đặc biệt (đọc từ write model hoặc optimistic UI).
Với dashboard hoặc báo cáo phân tích, lag 1 giây trở lên thường chấp nhận được. Với thao tác tài chính, ngưỡng SLO chặt hơn nhiều và cần thiết kế riêng cho read-your-writes — trường hợp người dùng cần xem kết quả của chính hành động họ vừa thực hiện.
5. Khi nào nên dùng CQRS — và khi nào không?
Martin Fowler — người phổ biến CQRS (thuật ngữ do Greg Young đặt) — viết vào năm 2011:
"Like any pattern, CQRS is useful in some places, but not in others. Many systems do fit a CRUD mental model, and so should be done in that style. CQRS is a significant mental leap for all concerned, so shouldn't be tackled unless the benefit is worth the jump."
Và cảnh báo quan trọng hơn:
"Despite these benefits, you should be very cautious about using CQRS. ... I've certainly seen cases where it's made a significant drag on productivity, adding an unwarranted amount of risk to the project, even in the hands of a capable team."
Dựa vào những gì bạn vừa đọc về CQRS vs CRUD và eventual consistency, thử liệt kê 2 dấu hiệu cho thấy hệ thống đủ lớn để CQRS xứng đáng. Ghi ra rồi đối chiếu với bảng dưới.
| Dấu hiệu | Khuyến nghị |
|---|---|
| Read load lớn hơn write rõ rệt (vd đọc 1.000 lần nhiều hơn ghi) | CQRS có thể hợp lý |
| Nhiều loại truy vấn đọc cần schema/index khác nhau | CQRS có thể hợp lý |
| Nhiều client (dashboard, mobile, analytic) cần view khác nhau từ cùng dữ liệu | CQRS có thể hợp lý |
| Hệ thống nhỏ/vừa, team nhỏ, nghiệp vụ chưa phức tạp | Giữ CRUD — CQRS thêm overhead không tương xứng |
| Read và write load tương đương | Giữ CRUD + replica đọc |
| Cần strong consistency cho mọi thao tác | Không dùng CQRS thuần |
Đáp án: (1) read load lớn hơn write rõ rệt, hoặc (2) nhiều loại truy vấn đọc cần schema/index riêng không thể chia sẻ. Nếu không có dấu hiệu nào trong số đó — CRUD thường là lựa chọn đúng.
CQRS không phải pattern mặc định. Với hầu hết ứng dụng CRUD thông thường, thêm CQRS là thêm complexity (projection, eventual consistency, nhiều data store) mà không có lợi tương xứng. Fowler khuyến cáo chỉ dùng CQRS cho bounded context mà read/write load thật sự khác nhau — không áp dụng toàn hệ thống.
6. CQRS có cần event sourcing không?
Không nhất thiết. Hai pattern độc lập nhau, dù hay xuất hiện cùng nhau:
- CQRS không event sourcing: write model ghi trạng thái cuối vào DB chuẩn hoá (row ghi đè), đồng thời publish event để projection cập nhật read model. Không có lịch sử sự kiện đầy đủ — chỉ có state hiện tại và các materialized view.
- CQRS + event sourcing: write model là event store (lưu chuỗi event bất biến như đã học ở bài trước). Projection replay event để dựng view. Kết hợp này cho phép thêm read model mới bất cứ lúc nào bằng cách replay toàn bộ event lịch sử — nhưng cộng thêm complexity của event sourcing.
Bài 02 (event sourcing) đã đi sâu vào cơ chế lưu event và replay. Bài này tập trung vào phần CQRS: tại sao tách read/write và hệ quả eventual consistency — hai câu hỏi đứng độc lập với event sourcing.
7. Pitfall tổng hợp
❌ Nhầm 1: Đọc read model ngay sau write — tưởng thấy dữ liệu mới
# Sequence khong an toan voi CQRS:
POST /transfer -> 200 OK (write model chap nhan)
GET /balance -> 500000 (read model chua cap nhat -- so du cu)
# Client nhin thay so du cu -> nghi chuyen tien that bai -> gui lai
# -> double-submit
✅ Với thao tác read-your-writes-critical: đọc từ write model trực tiếp, không đọc từ read model. Hoặc UI optimistic update (hiển thị kết quả ngay mà không chờ đọc lại từ server) cộng với version check để không phụ thuộc vào lag.
❌ Nhầm 2: Áp CQRS cho toàn hệ thống
Áp CQRS cho mọi aggregate, mọi endpoint — kể cả những chỗ chỉ có CRUD đơn giản — làm toàn hệ thống phức tạp mà không có lợi. Đây là lỗi phổ biến nhất mà Fowler ghi nhận.
✅ Chỉ dùng CQRS cho bounded context mà read/write load thật sự chênh lệch. Phần còn lại của hệ thống giữ CRUD thông thường.
❌ Nhầm 3: Tưởng CQRS cải thiện consistency — thực ra nó chủ động đánh đổi
CQRS không làm consistency tốt hơn. Nó đánh đổi strong consistency lấy khả năng scale read/write độc lập. Nếu ứng dụng cần strong consistency (giao dịch tài chính chặt, inventory chính xác), phải thiết kế đặc biệt cho read-your-writes hoặc không dùng CQRS thuần.
8. 📚 Deep Dive
Spec / reference chính thức:
- CQRS — Martin Fowler (2011) — bài viết định nghĩa và phổ biến CQRS (thuật ngữ do Greg Young đặt). Đọc toàn bài để hiểu lập luận gốc và cảnh báo cụ thể của Fowler về over-engineering với CQRS.
- Pattern: CQRS — microservices.io (Chris Richardson) — mô tả CQRS trong bối cảnh microservices: view database riêng (thường NoSQL), đồng bộ qua domain events; kèm liên kết với các pattern liên quan (event sourcing, saga, API gateway).
Ghi chú: đọc Fowler 2011 trước để có nhận thức đúng về khi nào không nên dùng, rồi đọc microservices.io cho implementation pattern trong hệ thống phân tán. Fowler viết từ góc nhìn thận trọng; Richardson mô tả cách làm khi đã quyết định dùng.
9. Liên hệ các bài khác
- Bài 02 — Event sourcing: event sourcing lưu chuỗi event bất biến thay vì state — thường kết hợp với CQRS để projection dựng read model bằng replay. Bài 02 giải thích write model; bài này giải thích tại sao cần tách read model.
- Bài 01 — CDC: CDC là một cơ chế cập nhật read model — thay vì viết projection code lắng nghe domain event, có thể dùng CDC đọc changelog của write DB và đẩy vào read model bất đồng bộ.
- Bài 04 — Lambda và Kappa: lambda/kappa là kiến trúc kết hợp batch và stream để xây derived data ở quy mô lớn — read model của CQRS là một dạng derived data đặc biệt; bài 04 mở rộng tư duy này lên toàn hệ thống.
10. Tóm tắt
- CQRS: write model nhận command, thay đổi state; read model = materialized view bất đồng bộ, lag thông thường ~50ms, tail vượt 2 giây khi tải cao.
- Eventual consistency là đặc điểm thiết kế có chủ đích — read-after-write có thể trả dữ liệu cũ hoặc 404.
- Với read-your-writes-critical (xem số dư ngay sau chuyển tiền): đọc từ write model trực tiếp hoặc dùng optimistic UI update.
- Chỉ dùng khi read/write load thật sự khác nhau (Fowler 2011); CQRS và event sourcing độc lập — không nhất thiết kết hợp.
11. Tự kiểm tra
- Q1CQRS tách gì khỏi gì? Định nghĩa ngắn gọn command và query trong CQRS, và cho một ví dụ cụ thể cho mỗi loại.
- Q2Materialized view trong CQRS khác gì so với DB view thông thường (CREATE VIEW trong SQL)?
- Q3Vì sao đọc read model ngay sau write có thể trả về dữ liệu cũ hoặc 404? Giải thích cơ chế, kèm ví dụ incident double-submit trong hệ thống payment.
- Q4So sánh CQRS với CRUD truyền thống: nêu một điểm mạnh và một điểm yếu rõ ràng của mỗi mô hình. Khi nào nên chọn CQRS thay CRUD?
- Q5Hệ thống dùng CQRS cho trang hồ sơ người dùng. Người dùng vừa đổi ảnh đại diện, reload trang ngay và vẫn thấy ảnh cũ. Đây là lỗi hay hành vi bình thường? Cách xử lý phù hợp nhất là gì?
- Q6CQRS và event sourcing có bắt buộc đi cùng nhau không? Mô tả ngắn gọn CQRS không có event sourcing, và event sourcing không có CQRS.
- Q7Martin Fowler khuyên rất thận trọng với CQRS vì nó có thể làm giảm năng suất đáng kể. Nêu hai dấu hiệu cụ thể cho thấy hệ thống không cần CQRS, và giải thích vì sao thêm CQRS vào sẽ làm hại.
Bài tiếp theo: Lambda và Kappa — kết hợp batch và stream
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên