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

  1. Read scaling: Khi nào cần thêm RDS read replica hoặc ElasticSearch read model cho Fluxion?
  2. Event Sourcing: Có cần full audit trail cho action_executions không — nếu có, khi nào thêm event store?
  3. Eventual consistency: Độ trễ chấp nhận được của AppSync subscription trong production là bao nhiêu?