T3 — CQRS Pattern - congsinhv/fluxion GitHub Wiki
CQRS (Command Query Responsibility Segregation) Pattern
Issue: #14 — Nghiên cứu CQRS Pattern Tuần: 3 | 07/04 – 13/04/2026 Numbering chính thức: Mục 2.6 theo Master TOC (Chương 2)
2.6 CQRS Pattern và Ứng Dụng trong Fluxion MDM
2.6.1 Từ CQS Đến CQRS
Command Query Separation (CQS) được đề xuất bởi Bertrand Meyer trong Object Oriented Software Construction [5]. Nguyên tắc cốt lõi:
- Queries: Trả về dữ liệu mà không thay đổi trạng thái (no side-effect)
- Commands: Thay đổi trạng thái nhưng không trả về giá trị
Ý tưởng nền tảng: "đặt một câu hỏi không nên thay đổi câu trả lời" [5].
CQRS là sự tổng quát hoá CQS từ mức phương thức lên mức kiến trúc hệ thống, được Martin Fowler công bố năm 2011 [4]:
"Sử dụng một mô hình khác nhau để cập nhật thông tin so với mô hình để đọc thông tin."
Thay vì một mô hình phục vụ cả đọc lẫn ghi, CQRS tách thành hai mô hình độc lập:
- Command Model: Xử lý write, thực thi quy tắc nghiệp vụ
- Query Model: Tối ưu cho read, trả về dữ liệu nhanh
Greg Young [6] nhấn mạnh tách biệt này cho phép tối ưu hoá và scale độc lập, đồng thời tích hợp tự nhiên với event-driven architecture.
2.6.2 So Sánh CRUD Truyền Thống vs CQRS
| Khía cạnh | CRUD truyền thống | CQRS |
|---|---|---|
| Mô hình dữ liệu | Một mô hình duy nhất | Hai mô hình tách biệt |
| Database | Đơn nhất (read & write) | Có thể tách (write DB + read store) |
| Độ phức tạp | Thấp | Cao (multiple models, eventual consistency) |
| Hiệu suất đọc | Phụ thuộc join/filter | Có thể tối ưu hoàn toàn |
| Consistency | Strong (mặc định) | Eventual (tuỳ triển khai) |
| Scaling | Cả read/write cùng scale | Scale độc lập per path |
| Phù hợp cho | MVP, app đơn giản | Domain phức tạp, tỉ lệ read:write lệch |
Bảng 2.6.1: So sánh CRUD và CQRS
2.6.3 Khi Nào Nên / Không Nên Dùng CQRS
NÊN dùng khi [4][6]:
- Domain nghiệp vụ phức tạp — logic write khác biệt hoàn toàn so với logic read
- Tỉ lệ read:write lệch lớn (ví dụ: 100:1) — cần scale độc lập
- Đã dùng event-driven architecture — CQRS tích hợp tự nhiên
- Cần audit trail — kết hợp CQRS + Event Sourcing
KHÔNG NÊN dùng khi [4]:
"For most systems CQRS adds risky complexity." — Martin Fowler [4]
- App CRUD đơn giản (~100 users, model đơn giản)
- Strong consistency bắt buộc — CQRS mang lại eventual consistency
- Lợi ích chưa rõ ràng so với chi phí phức tạp
2.6.4 CQRS trong Kiến Trúc Fluxion
Fluxion sử dụng simplified CQRS — tách command/query path tại mức Lambda resolver, dùng chung một RDS PostgreSQL (không có separate read replica ở giai đoạn MVP).
Command Path (Write Side)
GraphQL Mutation (assignAction)
→ AppSync (Cognito auth)
→ action-resolver Lambda (validate + enqueue)
→ action-trigger-sqs
→ action-trigger Lambda
├─ INSERT action_executions (ACTION_PENDING) vào RDS
└─ Publish → device-event-sns
→ ios-process-action Lambda (gửi APNS)
→ iOS Device (apply action)
→ checkin-ecs (UPDATE action_executions → ACTION_COMPLETED)
Query Path (Read Side)
GraphQL Query (getDevices, getActions, getUsers)
→ AppSync (Cognito auth)
→ Lambda resolver theo domain:
├─ device-resolver → SELECT từ RDS
├─ user-resolver → SELECT từ RDS
└─ platform-resolver → SELECT từ RDS
→ Trả về JSON cho client
Real-time Path (Subscription Side)
GraphQL Subscription (onActionStatusChanged)
→ AppSync duy trì WebSocket connection
→ Khi action-trigger/checkin-ecs publish event → SNS → AppSync
→ AppSync push update → Client
AppSync subscription là real-time native — không cần Redis hay WebSocket server riêng.
Tại Sao CQRS Phù Hợp Fluxion
| Đặc điểm Fluxion | Lợi ích CQRS |
|---|---|
| action-resolver chỉ validate + enqueue | Command path thuần, không lẫn read logic |
| domain resolvers chỉ SELECT | Query path thuần, không lẫn write logic |
| SQS/SNS async | Command side bất đồng bộ tự nhiên |
| AppSync subscriptions | Real-time mà không cần read model riêng |
| Thêm subscriber mới vào SNS | Mở rộng command side không ảnh hưởng query side |
Bảng 2.6.2: Fluxion đặc điểm → lợi ích CQRS
2.6.5 CQRS + Saga: Tích Hợp với Command Pipeline
Command side của CQRS là entry point của Choreography Saga (mục 2.5.1–2.5.11):
[CQRS Command Side] → [Saga Trigger]
GraphQL Mutation → action-resolver (CQRS: validate write)
→ action-trigger-sqs
→ action-trigger Lambda ← Saga Step 1: INSERT action_executions
→ device-event-sns
→ ios-process-action Lambda ← Saga Step 2: APNS + UPDATE ACTION_SENT
→ checkin-ecs ← Saga Step 3: UPDATE ACTION_COMPLETED
[CQRS Query Side] → Client truy vấn action_executions.state bất kỳ lúc nào
[CQRS Real-time] → AppSync push khi state thay đổi
CQRS đảm bảo:
- Command path xử lý Saga steps (ghi, event publish)
- Query path cho client theo dõi Saga state hiện tại
- Subscription path notify client khi Saga chuyển state
2.6.6 Sơ Đồ CQRS Data Flow trong Fluxion
flowchart TD
subgraph Client
M["GraphQL Mutation\n(assignAction)"]
Q["GraphQL Query\n(getDevices / getActions)"]
S["GraphQL Subscription\n(onActionStatusChanged)"]
end
subgraph CMD["COMMAND PATH"]
AR["action-resolver Lambda\nvalidate + enqueue"]
SQS["action-trigger-sqs"]
AT["action-trigger Lambda\nINSERT action_executions"]
SNS["device-event-sns"]
IPA["ios-process-action Lambda\nAPNS send"]
ECS["checkin-ecs\nFSM update"]
end
subgraph QRY["QUERY PATH"]
DR["device-resolver Lambda"]
UR["user-resolver Lambda"]
PR["platform-resolver Lambda"]
end
subgraph RT["REAL-TIME (Subscription)"]
APPSYNC_RT["AppSync\nWebSocket Push"]
end
RDS[("RDS PostgreSQL")]
M --> AR --> SQS --> AT --> SNS --> IPA --> ECS
AT -->|write| RDS
ECS -->|write| RDS
Q --> DR & UR & PR
DR & UR & PR -->|read| RDS
S --> APPSYNC_RT
SNS -->|event| APPSYNC_RT
APPSYNC_RT -->|push| S
style CMD fill:#dbeafe
style QRY fill:#f3e8ff
style RT fill:#dcfce7
Hình 2.4: CQRS data flow trong Fluxion — Command (xanh), Query (tím), Real-time (xanh lá)
2.6.7 CQRS và Event Sourcing — Hai Pattern Bổ Sung
CQRS và Event Sourcing thường đi cùng nhau nhưng là hai pattern độc lập [7]:
| CQRS | Event Sourcing | |
|---|---|---|
| Giải quyết | Read/write scaling | Audit trail, state reconstruction |
| Database | Relational DB | Event store (append-only log) |
| Có thể dùng độc lập | ✓ | ✓ |
Fluxion hiện tại dùng CQRS mà không có Event Sourcing. Nếu cần full audit trail (lịch sử mọi command), có thể thêm event store sau — không ảnh hưởng command/query path hiện tại.
Lưu ý quan trọng: Fluxion áp dụng nguyên tắc Tách biệt Lệnh-Truy vấn (Command-Query Separation — CQS) tại tầng Lambda resolver, lấy cảm hứng từ CQRS pattern. Hệ thống sử dụng chung một cơ sở dữ liệu PostgreSQL (không có separate read model), do đó đây là simplified CQS chứ không phải CQRS đầy đủ theo định nghĩa của Fowler (2011). Việc áp dụng mức độ này phù hợp với nguyên tắc KISS và quy mô của đồ án tốt nghiệp.
2.6.8 Kết Luận
CQRS là công cụ có mục đích — không phải giải pháp phổ quát. Fowler cảnh báo rõ về complexity risk [4]. Với Fluxion:
- ✅ Simplified CQRS phù hợp: tách path tại Lambda level, không over-engineer
- ✅ Hỗ trợ async SQS/SNS pipeline tự nhiên
- ✅ AppSync subscriptions xử lý real-time — không cần Redis hay WebSocket server riêng
- ✅ Dễ mở rộng: thêm read replica, ElasticSearch, Redis cache sau mà không thay đổi command logic
- ⚠️ Dùng chung một RDS → bottleneck khi read scale lớn (giải pháp: thêm read replica)
Tài Liệu Tham Khảo
[2] Kleppmann, M. (2017). Designing Data-Intensive Applications, Ch. 11–12. O'Reilly Media. https://dataintensive.net/
[4] Fowler, M. (2011). CQRS. martinfowler.com. https://martinfowler.com/bliki/CQRS.html
[5] Fowler, M. Command Query Separation. martinfowler.com. https://martinfowler.com/bliki/CommandQuerySeparation.html
[6] Young, G. (2010). CQRS Documents. https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
[7] Microsoft Azure Architecture Center. (2023). CQRS Pattern. https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs
[8] Richardson, C. Pattern: CQRS. microservices.io. https://microservices.io/patterns/data/cqrs.html
Câu hỏi còn mở:
- Read scaling: Khi nào cần thêm RDS read replica hoặc ElasticSearch read model cho Fluxion?
- Event Sourcing: Có cần full audit trail cho action_executions không — nếu có, khi nào thêm event store?
- Eventual consistency: Độ trễ chấp nhận được của AppSync subscription trong production là bao nhiêu?