Sau giờ ăn trưa, một dashboard sản phẩm tải chậm hơn bình thường. Cùng một bảng database đang phải làm hai việc rất khác nhau: bảo vệ toàn bộ rule khi thay đổi một đơn hàng, và trả lời một query rộng nối customer, payment, discount, shipment, support data. Không ai cố tình làm hệ thống rối. Chỉ là một model đã bị giao cả việc ghi cẩn thận lẫn việc đọc tiện lợi.
CQRS là viết tắt của Command Query Responsibility Segregation. Tên nghe nặng, nhưng ý chính khá nhỏ: tách phần thay đổi trạng thái khỏi phần đọc trạng thái. Command yêu cầu hệ thống làm một việc: tạo order, duyệt invoice, hủy booking. Query hỏi hệ thống đang biết gì: hiển thị order summary, liệt kê invoice quá hạn, render dashboard. Hai hành động đó chịu hai áp lực khác nhau, và đôi khi cần hai hình dạng khác nhau.
Ở phía command, model nên bảo vệ business rule. Nó quan tâm đến validation, invariant, permission, idempotency, transaction và failure rõ ràng. Nó không nhất thiết phải thuận tiện cho mọi màn hình. Điều đó ổn. Việc của nó không phải là chiều theo từng UI. Việc của nó là giữ hệ thống đúng khi có thứ gì đó thay đổi.
Ở phía query, model có thể được tạo hình cho việc đọc. Nó có thể denormalized, cached, indexed, precomputed, hoặc lưu theo đúng cấu trúc mà màn hình cần. Một trang order detail có thể cần customer name, payment status, shipment timeline, refund state và support flags trong một response. Thay vì ép write model thành reporting warehouse, một read model có thể giữ sẵn view đó và cập nhật khi các sự thật bên dưới thay đổi.
Điều này không có nghĩa mọi hệ thống CQRS đều cần hai database, event sourcing, Kafka, hay một sơ đồ đầy mũi tên. Ở dạng nhẹ nhất, CQRS có thể chỉ là command handler và query handler tách nhau trong cùng codebase, dùng cùng database. Sự tách biệt quan trọng trước hết là về tư duy. Đừng trộn một write operation với logic reporting ẩn. Đừng để một query path vô tình thay đổi state. Khi áp lực lớn hơn, phần tách vật lý có thể đến sau.
CQRS hữu ích khi phần đọc và phần ghi thật sự khác nhau. Một product catalog có thể được đọc hàng nghìn lần cho mỗi lần update. Một workflow tài chính có thể có rule ghi rất chặt nhưng nhu cầu reporting rất rộng. Một customer support console có thể cần một view nhanh ghép data từ nhiều service. Trong những trường hợp đó, bắt một model vừa nghiêm khắc vừa tiện lợi có thể khiến nó tệ ở cả hai phía.
Cái giá cũng thật. Khi read model tách riêng, team phải giữ nó cập nhật. Nếu nó được nuôi bằng event, event có thể trễ, lặp, hoặc mất nếu pipeline yếu. UI có thể trong chốc lát hiển thị data cũ. Engineer cần biết sự thật nằm ở đâu, read model được rebuild thế nào, độ mới được đo ra sao, và chuyện gì xảy ra khi projector lỗi. CQRS đổi một loại phức tạp lấy một loại phức tạp khác. Chỉ nên đổi khi cái phức tạp cũ đã thật sự gây đau.
Sai lầm phổ biến nhất là dùng CQRS vì nó nghe có vẻ senior. Một màn hình CRUD đơn giản với một bảng và traffic vừa phải thường không cần nó. Một admin tool nhỏ có thể tử tế hơn với một model, một transaction, và test rõ ràng. Architecture nên gỡ đau, không nên tạo danh tính. Nếu không ai gọi tên được nỗi đau ở phía đọc hoặc phía ghi, CQRS có lẽ còn quá sớm.
Một cách áp dụng thực tế nên khiêm tốn. Đầu tiên tách command function khỏi query function. Sau đó đặt tên command rõ theo ý định nghiệp vụ, như ApproveRefund hay CancelSubscription. Tiếp theo, tạo một read shape cho đúng một màn hình đang đau, không phải cho cả hệ thống. Thêm observability cho update lag và rebuild. Giữ write model đơn giản và được bảo vệ. Như vậy team học pattern mà không đặt cược cả sản phẩm vào nó.
CQRS cũng thay đổi cuộc trò chuyện với product và support. Một read model có thể eventually consistent, nên team cần biết người dùng có chấp nhận thấy status cũ trong một giây không. Một số màn hình chấp nhận được. Một số thì không. Trang xác nhận checkout có thể cần bảo đảm mạnh hơn dashboard analytics. Quyết định architecture một phần thuộc về khoảnh khắc nghiệp vụ, không chỉ thuộc về code.
Giá trị lặng lẽ của CQRS là nó cho mỗi phía một công việc thành thật hơn. Command nói về ý định và rule. Query nói về khả năng nhìn thấy và sự hữu ích. Khi hai việc đó còn nhỏ, cứ để chúng ở gần nhau. Khi chúng kéo ngược nhau, hãy tách ra một cách cẩn thận.
Lần tới khi một model có vẻ bị kéo căng, hãy hỏi phía nào đang đau. Việc ghi có đang rủi ro vì field phục vụ reporting cứ rò vào domain không? Việc đọc có đang chậm vì database tối ưu cho transaction không? Câu trả lời chưa chắc là CQRS đầy đủ. Có thể chỉ là một read model nhỏ, một command boundary rõ hơn, và một team cuối cùng thôi bắt một bảng phục vụ mọi mục đích cùng lúc.