Files
cn-ui/specs/Event_Processor_HLD_v2.md

30 KiB

Event Processor Service — High-Level Design v2

Event-Driven Loyalty & Rewards Platform


1. Mục tiêu & Phạm vi

1.1. Mục tiêu

  • Chuẩn hóa luồng xử lý event: Các event phi tài chính đi qua event-processor-service để validate, enrich và route.
  • Bảo vệ Transaction Service (TP): TP chỉ xử lý command/event có tác động tài chính, voucher, inventory hoặc reward rule.
  • Tách tracking khỏi core ledger: Các counter, streak, trạng thái hiển thị và thuộc tính động được xử lý tại attribute-service.
  • Đảm bảo đúng dữ liệu tài chính: Mọi nghiệp vụ cộng/trừ điểm, cấp voucher, đổi trạng thái voucher, reserve inventory phải nằm trong transaction-service và transaction DB.
  • Cho phép cấu hình chiến dịch phổ biến mà không cần sửa code: Operation catalog chỉ giữ các primitive cần thiết cho phần lớn use case hiện tại, tránh tạo quá nhiều operation sớm.

1.2. Nguyên tắc thiết kế

  1. Direct Flow khác Direct Event.
    • Direct Flow: Command cần phản hồi ngay cho user/merchant và cần ACID mạnh. Client gọi thẳng API của transaction-service.
    • Direct Event: Event bất đồng bộ cần TP xử lý rule/reward, nhưng không cần response sync cho caller. Producer publish vào Kafka events-topic.
  2. Transaction Service là nơi duy nhất thực hiện nghiệp vụ tài chính: cộng/trừ điểm, cấp voucher, reserve inventory, dùng voucher, giới hạn thưởng.
  3. Attribute Service chỉ lưu state và chạy primitive operations. Service này không quyết định "thưởng bao nhiêu"; khi điều kiện tracking đạt threshold, nó phát sinh một Direct Event mới để transaction-service xử lý rule/reward.
  4. Event Processor là smart gateway cho event async: validate schema, tra registry, enrich payload, route vào topic tương ứng.
  5. Marketing Service chỉ lập lịch, quét tập khách hàng và phát sinh event/notification. Không cộng điểm hay cấp voucher trực tiếp.
  6. Mọi publish quan trọng sau khi ghi DB phải dùng Outbox Pattern để tránh mất event hoặc duplicate do crash/retry.

2. Kiến trúc Tổng quan

graph TD
    subgraph Client Layer
        MA[Member App]
        MRA[Merchant App]
        EXT[External Systems]
    end

    subgraph Gateway Layer
        EP[event-processor-service<br/>Validate + Registry + Route]
        API[TP Sync API]
    end

    subgraph Kafka
        ET[events-topic<br/>Direct Events / Reward Triggers]
        TT[tracking-topic<br/>Tracking Events]
        NT[noti-topic<br/>Notification Events]
        DLQ[dead-letter-topics]
    end

    subgraph Core Services
        TP[transaction-service<br/>Rule + Ledger + Inventory]
        ATTR[attribute-service<br/>State + Counters]
        NS[notification-service]
        MKT[marketing-service]
    end

    subgraph Data Layer
        DB_TP[(PostgreSQL<br/>Transaction DB)]
        DB_ATTR[(PostgreSQL<br/>Attribute DB)]
        REDIS[(Redis<br/>Cache)]
        DORIS[(Doris<br/>BI / Reporting)]
    end

    MA -->|Tracking / Notification Events| EP
    EXT -->|Async Direct Events| EP
    MKT -->|Scheduled Events| EP

    MA -->|Direct Flow: Redeem / Transfer| API
    MRA -->|Direct Flow: Scan Voucher| API
    API --> TP

    EP -->|Direct Event| ET
    EP -->|Tracking Event| TT
    EP -->|Notification Event| NT

    ET --> TP
    TT --> ATTR
    NT --> NS

    ATTR -->|Threshold Trigger<br/>via Outbox| ET
    TP -->|Reward / Txn Result<br/>via Outbox| NT
    TP -->|Post-transaction Tracking Event<br/>via Outbox| TT

    TP --- DB_TP
    ATTR --- DB_ATTR
    ATTR -. recently viewed .-> REDIS
    DB_TP -. CDC/ETL .-> DORIS
    DB_ATTR -. CDC/ETL .-> DORIS

    ET -. failed .-> DLQ
    TT -. failed .-> DLQ
    NT -. failed .-> DLQ

2.1. Event Categories

Category Khi nào dùng Đường đi chính Ví dụ
Direct Flow User/merchant cần kết quả ngay, có ACID tài chính/inventory Client/Merchant -> transaction-service API Transfer_Balance, Redeem_Item, Scan_Voucher
Direct Event Async event cần TP xử lý reward/rule, không cần sync response. Có thể đến từ producer bên ngoài hoặc được sinh ra bởi Attribute khi Tracking Event đạt threshold Producer -> EP -> events-topic -> TP; hoặc Attribute outbox -> events-topic -> TP Welcome_Offer, Profile_Completed, Dormant_User, Login_Streak_7_Days
Tracking Event Cần đếm, tích lũy, set cờ, lưu trạng thái phi tài chính Producer -> event-processor-service -> tracking-topic -> attribute-service Login, View_Item, Survey_Completed, Item_Redeemed_Count
Notification Event Chỉ gửi thông báo, không cần ledger/counter Producer -> event-processor-service hoặc service outbox -> noti-topic -> notification-service Password_Expiring, transaction success notification

2.2. Quan hệ Tracking Event và Direct Event phát sinh

Tracking Event không trực tiếp yêu cầu TP cộng điểm/cấp voucher. Nó chỉ cập nhật state tại Attribute. Khi state đạt threshold, Attribute tạo ra Generated Direct Event mới và publish vào events-topic.

Ví dụ:

Tracking Event: Login
  -> Attribute cập nhật login_streak = 7
  -> Threshold Login_Streak_7_Days đạt
  -> Attribute publish Direct Event: Login_Streak_7_Days
  -> Transaction Service consume Direct Event và cộng điểm theo rule

Quy ước:

  • Direct Event phát sinh từ threshold phải có eventCategory = "DIRECT".
  • source nên là attribute-service.
  • Payload phải giữ sourceEventId hoặc causedByEventId để trace ngược về Tracking Event gốc.
  • TP xử lý Generated Direct Event giống các Direct Event async khác: check idempotency, eligibility, campaign rule, rồi mới apply reward.

3. Luồng Xử lý Chuẩn

3.1. Tracking Flow — Accumulate & Trigger

sequenceDiagram
    participant MA as Member App
    participant EP as Event Processor
    participant TT as tracking-topic
    participant ATTR as Attribute Service
    participant DB as Attribute DB
    participant OUT as Attribute Outbox
    participant ET as events-topic
    participant TP as Transaction Service

    MA->>EP: Thin Event: Login / View_Item
    EP->>EP: Validate schema + load registry
    EP->>TT: Fat Payload with instructions
    TT->>ATTR: Consume
    ATTR->>DB: BEGIN transaction
    ATTR->>DB: Update attribute state
    ATTR->>DB: Insert threshold_fire_log if threshold reached
    ATTR->>OUT: Insert Generated Direct Event into outbox
    ATTR->>DB: COMMIT
    OUT->>ET: Publish Generated Direct Event
    ET->>TP: Consume Direct Event and apply reward rule

Điểm bắt buộc:

  • Update attribute, insert threshold_fire_log, và insert outbox record phải nằm trong cùng DB transaction.
  • Generated Direct Event chỉ publish từ outbox sau khi commit thành công.
  • Generated Direct Event là event mới, có eventId riêng, nhưng phải tham chiếu Tracking Event gốc qua causedByEventId.
  • Nếu publish lỗi, outbox worker retry; không rollback state đã xử lý.

3.2. Direct Flow — Sync Financial Command

Áp dụng cho nghiệp vụ user/merchant cần biết kết quả ngay: chuyển điểm, đổi quà, quét voucher.

sequenceDiagram
    participant APP as Member/Merchant App
    participant TP as Transaction Service
    participant DB as Transaction DB
    participant OUT as TP Outbox
    participant NT as noti-topic
    participant TT as tracking-topic

    APP->>TP: Sync API command
    TP->>DB: BEGIN transaction
    TP->>DB: Validate balance / voucher / inventory
    TP->>DB: Update ledger + inventory/voucher state
    TP->>OUT: Insert notification/tracking events
    TP->>DB: COMMIT
    TP-->>APP: Return success/failure
    OUT->>NT: Publish notification event
    OUT->>TT: Publish post-transaction tracking event if needed

Ví dụ:

  • Redeem_Item: App gọi TP sync API. Sau khi redeem thành công, TP publish Item_Redeemed_Count vào tracking-topic để Attribute tăng total_redeem_count.
  • Transfer_Balance: App gọi TP sync API. Sau commit, TP publish notification cho người gửi và người nhận.
  • Scan_Voucher: Merchant App gọi TP sync API. Sau commit, TP publish notification cho customer.

3.3. Direct Event Flow — Async Reward/Rule

Áp dụng cho event không cần trả kết quả tức thì cho caller nhưng cần TP xử lý rule/reward.

sequenceDiagram
    participant P as Producer
    participant EP as Event Processor
    participant ET as events-topic
    participant TP as Transaction Service
    participant DB as Transaction DB
    participant OUT as TP Outbox
    participant NT as noti-topic

    P->>EP: Direct Event: Welcome_Offer / Profile_Completed / Dormant_User
    EP->>EP: Validate + route
    EP->>ET: Publish Direct Event
    ET->>TP: Consume
    TP->>DB: BEGIN transaction
    TP->>DB: Check idempotency + apply reward rule
    TP->>OUT: Insert notification event
    TP->>DB: COMMIT
    OUT->>NT: Publish notification

Direct Event async có 2 nguồn hợp lệ:

  • Producer-originated Direct Event: service/hệ thống ngoài gửi event vào EP, ví dụ Welcome_Offer, Profile_Completed, Dormant_User.
  • Threshold-generated Direct Event: Attribute sinh event khi Tracking Event đạt threshold, ví dụ First_Login_Reward, Login_Streak_7_Days, View_10_Items_Weekly, Survey_Completed_Reward, Account_Reactivated.

3.4. Notification Flow

  • Notification thuần túy đi thẳng noti-topic.
  • Notification phát sinh từ transaction phải được TP ghi vào outbox cùng transaction DB.
  • Notification Service cần idempotency theo eventId để tránh gửi trùng khi Kafka retry.

4. Event Envelope Chuẩn

Tất cả event đi qua Kafka nên có envelope thống nhất để trace, replay và idempotency.

{
  "eventId": "evt-20260716-abc123",
  "eventName": "Login",
  "eventCategory": "TRACKING",
  "schemaVersion": 1,
  "source": "member-app",
  "occurredAt": "2026-07-16T10:30:00+07:00",
  "publishedAt": "2026-07-16T10:30:02+07:00",
  "correlationId": "corr-789",
  "idempotencyKey": "member-login:CUST-001:2026-07-16",
  "actor": {
    "customerId": "CUST-001"
  },
  "entities": {
    "itemId": "ITEM-789"
  },
  "payload": {}
}
Field Bắt buộc Mục đích
eventId Yes Unique event id, dùng cho dedup, DLQ, audit
eventName Yes Tên event canonical, ví dụ Login, View_Item
eventCategory Yes DIRECT, TRACKING, NOTIFICATION
schemaVersion Yes Cho phép evolve schema
source Yes Producer/service phát event
occurredAt Yes Event time
publishedAt Yes Processing/publish time
correlationId Yes Trace một flow end-to-end
idempotencyKey Yes Business dedup key
actor Theo event Customer/member/merchant liên quan
entities Theo event Item, voucher, card, survey...
payload Theo event Data nghiệp vụ riêng

5. Registry & Instruction Model

5.1. Registry

Registry nằm tại event-processor-service, định nghĩa:

  • Event name và category.
  • Target topic.
  • Required fields/schema.
  • Instruction list cho tracking event.
  • Transaction code cho direct event.
  • Notification template cho notification event.

Ví dụ registry cho Login:

{
  "eventName": "Login",
  "eventCategory": "TRACKING",
  "targetTopic": "tracking-topic",
  "requiredFields": ["actor.customerId", "occurredAt"],
  "partitionKeyExpression": "Customer:${actor.customerId}",
  "instructions": [
    {
      "name": "First Login Bonus Gate",
      "op": "SET_TRUE_ONCE",
      "entity": "Customer",
      "entityId": "${actor.customerId}",
      "key": "first_login_flag",
      "triggerEventOnSuccess": "First_Login_Reward"
    },
    {
      "name": "Daily Login Streak",
      "op": "INCREMENT",
      "entity": "Customer",
      "entityId": "${actor.customerId}",
      "key": "login_streak",
      "val": 1,
      "mode": "STREAK",
      "period": "DAILY",
      "thresholds": [
        {
          "thresholdId": "Login_Streak_7_Days",
          "evaluateOn": "AFTER_UPDATE",
          "operator": "==",
          "left": "CURRENT_VALUE",
          "target": 7,
          "triggerEvent": "Login_Streak_7_Days",
          "triggerPolicy": "ONCE_LIFETIME"
        },
        {
          "thresholdId": "Login_Streak_30_Days",
          "evaluateOn": "AFTER_UPDATE",
          "operator": "==",
          "left": "CURRENT_VALUE",
          "target": 30,
          "triggerEvent": "Login_Streak_30_Days",
          "triggerPolicy": "ONCE_LIFETIME"
        }
      ]
    },
    {
      "name": "Inactive Account Monitor",
      "op": "SET",
      "entity": "Customer",
      "entityId": "${actor.customerId}",
      "key": "last_login_date",
      "val": "${occurredAt}",
      "thresholds": [
        {
          "thresholdId": "Account_Reactivated_90D",
          "evaluateOn": "BEFORE_UPDATE",
          "operator": "TIME_GAP_GT",
          "left": "OLD_VALUE",
          "right": "${occurredAt}",
          "target": 90,
          "unit": "DAY",
          "triggerEvent": "Account_Reactivated",
          "triggerPolicy": "ONCE_PER_PERIOD",
          "period": "YEARLY"
        }
      ]
    }
  ]
}

5.2. Instruction Format

{
  "name": "Weekly View Count",
  "op": "INCREMENT",
  "entity": "Customer",
  "entityId": "${actor.customerId}",
  "key": "weekly_view_count",
  "val": 1,
  "mode": "WINDOW",
  "period": "WEEKLY",
  "thresholds": [
    {
      "thresholdId": "View_10_Items_Weekly",
      "evaluateOn": "AFTER_UPDATE",
      "operator": ">=",
      "left": "CURRENT_VALUE",
      "target": 10,
      "triggerEvent": "View_10_Items_Weekly",
      "triggerPolicy": "ONCE_PER_PERIOD",
      "period": "WEEKLY"
    }
  ]
}
Field Bắt buộc Ghi chú
name Yes Tên dễ đọc cho BA/operation
op Yes SET, SET_TRUE_ONCE, INCREMENT, APPEND_UNIQUE
entity Yes Customer, Item, Voucher, Survey...
entityId Yes ID object được update state
key Yes Attribute key
val Theo op Giá trị input
mode Optional Chỉ dùng cho INCREMENT: LIFETIME, WINDOW, STREAK
period Optional DAILY, WEEKLY, MONTHLY, YEARLY
thresholds Optional Mảng threshold. Mỗi threshold tự khai báo evaluateOnBEFORE_UPDATE hoặc AFTER_UPDATE
triggerEventOnSuccess Optional Dùng cho state transition như SET_TRUE_ONCE

6. Operation Catalog Phase 1

Thiết kế phase 1 chỉ cần 4 primitive operations. Đây là bộ đủ cho phần lớn nghiệp vụ loyalty thực tế: first action, streak, periodic counter, total counter, timer gap và recent list.

6.1. SET

Ghi đè giá trị attribute.

Thuộc tính Giá trị
Kiểu value String, Number, Boolean, DateTime
Idempotent Không mặc định; phụ thuộc idempotencyKey event
Use cases UC-05 last_login_date, các mốc thời gian/state đơn giản

Với bài toán inactive login, SET dùng threshold có evaluateOn = BEFORE_UPDATE: check khoảng cách giữa occurredAt và old value trước, sau đó mới ghi value mới.

old last_login_date = 2026-04-15
event occurredAt    = 2026-07-16
TIME_GAP_GT 90 days = true
=> emit Account_Reactivated
=> SET last_login_date = 2026-07-16

6.2. SET_TRUE_ONCE

Chuyển Boolean từ false/null/not_exists sang true đúng một lần.

Thuộc tính Giá trị
Kiểu value Boolean
Idempotent
Trigger Chỉ trigger khi state transition thành công
Use cases UC-01 First Login, UC-08 Survey Completed

Best practice: dùng operation này làm gate cho reward chỉ được nhận một lần. Với UC-08, chỉ khi survey_completed chuyển thành true thì mới emit Survey_Completed_Reward.

6.3. INCREMENT

Cộng số theo 3 mode.

Mode Ý nghĩa Use cases
LIFETIME Counter tích lũy không reset UC-12 total_redeem_count, UC-03 item_view_count
WINDOW Counter reset theo kỳ cố định UC-03 weekly_view_count
STREAK Đếm chuỗi liên tiếp theo ngày/tuần/tháng UC-02 login_streak

Quy tắc:

  • period chỉ bắt buộc với WINDOWSTREAK.
  • STREAK DAILY bỏ qua event trùng ngày để login nhiều lần không tăng streak.
  • WINDOW WEEKLY tính theo ISO week, thứ Hai là ngày đầu tuần.
  • Nếu khác period/window, value reset về val, không reset về 0 rồi increment riêng.

6.4. APPEND_UNIQUE

Thêm phần tử vào list, có chống trùng và giới hạn độ dài.

Thuộc tính Giá trị
Kiểu value Array
Options maxSize, dedupeKey
Use cases UC-03 recently_viewed

Ví dụ:

{
  "op": "APPEND_UNIQUE",
  "entity": "Customer",
  "entityId": "${actor.customerId}",
  "key": "recently_viewed",
  "val": {
    "itemId": "${entities.itemId}",
    "viewedAt": "${occurredAt}"
  },
  "options": {
    "dedupeKey": "itemId",
    "maxSize": 50
  }
}

6.5. Không tạo operation riêng ở phase 1

Các operation sau chưa cần tách riêng vì 13 use case hiện tại xử lý được bằng 4 primitive trên:

  • STREAK_INCREMENT: dùng INCREMENT với mode = STREAK.
  • CHECK_AND_UPDATE_TIMER: dùng SET với threshold evaluateOn = BEFORE_UPDATE.
  • APPEND: dùng APPEND_UNIQUE; nếu cần cho phép trùng sau này, thêm option unique = false thay vì tạo op mới.
  • COMPUTE_GAP: dùng threshold operator TIME_GAP_GT trên old value và occurredAt.

7. Threshold & Trigger Policy

7.1. Threshold Format

{
  "thresholdId": "View_10_Items_Weekly",
  "evaluateOn": "AFTER_UPDATE",
  "operator": ">=",
  "left": "CURRENT_VALUE",
  "target": 10,
  "triggerEvent": "View_10_Items_Weekly",
  "triggerPolicy": "ONCE_PER_PERIOD",
  "period": "WEEKLY"
}
Field Bắt buộc Ghi chú
thresholdId Yes Stable id, không đổi khi đổi display name
evaluateOn Yes AFTER_UPDATE cho counter/state thông thường; BEFORE_UPDATE cho check giá trị cũ trước khi ghi
operator Yes ==, >=, >, TIME_GAP_GT
left Theo operator CURRENT_VALUE hoặc OLD_VALUE; UI có thể ẩn field này và tự set theo evaluateOn
right Theo operator Giá trị so sánh bên phải, ví dụ ${occurredAt} cho TIME_GAP_GT
target Yes Giá trị so sánh
triggerEvent Yes Tên Generated Direct Event emit vào events-topic
triggerPolicy Yes ONCE_LIFETIME hoặc ONCE_PER_PERIOD
period Khi cần Bắt buộc với ONCE_PER_PERIOD

evaluateOn giúp UI chỉ cần một Threshold Builder duy nhất:

  • Mặc định là AFTER_UPDATE, dùng cho INCREMENT, SET_TRUE_ONCE và phần lớn threshold.
  • BEFORE_UPDATE chỉ nên hiển thị khi operation là SET và attribute type là DateTime.
  • Với TIME_GAP_GT, UI có thể hiển thị đơn giản: "Giá trị cũ của last_login_date cách occurredAt lớn hơn 90 ngày", không cần bắt user chọn left/right.

7.2. Trigger Policy

Policy Ý nghĩa Ví dụ
ONCE_LIFETIME Một entity chỉ trigger một lần trong toàn bộ vòng đời First login, survey completed, welcome offer
ONCE_PER_PERIOD Một entity chỉ trigger một lần trong mỗi kỳ Weekly view reward, yearly inactive account reward

threshold_fire_log cần có đủ scope để enforce đúng policy.

Unique key for ONCE_LIFETIME:
  entity_type + entity_id + attribute_key + threshold_id

Unique key for ONCE_PER_PERIOD:
  entity_type + entity_id + attribute_key + threshold_id + period_type + period_key

Ví dụ period_key:

  • WEEKLY: 2026-W29
  • MONTHLY: 2026-07
  • YEARLY: 2026

7.3. Threshold Fire Log

threshold_fire_log
- id
- entity_type
- entity_id
- attribute_key
- threshold_id
- trigger_policy
- period_type
- period_key
- source_event_id
- trigger_event_id
- fired_at
- created_at

Quy trình:

  1. Operation thực thi xong và value thật sự thay đổi.
  2. Attribute Service evaluate threshold.
  3. Nếu đạt, insert threshold_fire_log bằng unique key theo policy.
  4. Nếu insert thành công, tạo Generated Direct Event và insert vào outbox.
  5. Nếu unique conflict, bỏ qua vì threshold đã fire trong scope đó.

7.4. Generated Direct Event Payload

Khi threshold đạt, Attribute không publish lại Tracking Event gốc. Nó tạo một Direct Event mới, có semantic rõ ràng cho TP.

{
  "eventId": "evt-direct-20260716-001",
  "eventName": "Login_Streak_7_Days",
  "eventCategory": "DIRECT",
  "schemaVersion": 1,
  "source": "attribute-service",
  "occurredAt": "2026-07-16T10:30:00+07:00",
  "publishedAt": "2026-07-16T10:30:05+07:00",
  "correlationId": "corr-789",
  "idempotencyKey": "reward:Login_Streak_7_Days:Customer:CUST-001",
  "causedByEventId": "evt-login-20260716-abc123",
  "actor": {
    "customerId": "CUST-001"
  },
  "entities": {
    "attributeKey": "login_streak"
  },
  "payload": {
    "thresholdId": "Login_Streak_7_Days",
    "attributeValue": 7,
    "triggerPolicy": "ONCE_LIFETIME"
  }
}

TP chỉ dựa vào eventName/transaction code/rule config để quyết định reward. Attribute không truyền số điểm/voucher cần cấp, trừ khi đó là metadata để audit.


8. Partitioning, Concurrency & Idempotency

8.1. Partition Key

Không partition cố định theo customerId. Partition key phải theo state được update.

partitionKey = entityType + ":" + entityId

Ví dụ:

Event State update Partition key
Login Customer streak/cờ login Customer:CUST-001
View_Item weekly count Customer weekly view Customer:CUST-001
View_Item item counter Item view count Item:ITEM-789
Item_Redeemed_Count Item redeem count Item:ITEM-789
Survey_Completed Customer survey flag Customer:CUST-001

Nếu một incoming event cần update nhiều entity khác nhau, Event Processor tách thành nhiều tracking messages, mỗi message có partition key riêng. Ví dụ View_Item có thể tạo:

  • Message 1: update Customer:CUST-001 cho weekly_view_countrecently_viewed.
  • Message 2: update Item:ITEM-789 cho item_view_count.

8.2. Idempotency

  • Mỗi service lưu processed event theo eventId hoặc idempotencyKey.
  • TP dùng business idempotency key cho transaction/reward để chống cộng điểm/cấp voucher trùng.
  • Attribute dùng event idempotency để tránh tăng counter trùng khi producer retry.
  • Notification Service dùng eventId hoặc notification dedupe key để tránh gửi thông báo trùng.

9. Use Case Mapping

UC Event/Command Category Flow Operation Trigger/Output
UC-01 First Login Login Tracking Event -> Generated Direct Event EP -> tracking-topic -> ATTR -> events-topic -> TP SET_TRUE_ONCE first_login_flag First_Login_Reward Direct Event -> +100đ + noti
UC-02 Daily Login Login Tracking Event -> Generated Direct Event EP -> tracking-topic -> ATTR -> events-topic -> TP INCREMENT login_streak mode=STREAK period=DAILY Login_Streak_7_Days, Login_Streak_30_Days Direct Events
UC-03 View Item View_Item Tracking Event, optional Generated Direct Event EP -> tracking-topic -> ATTR INCREMENT weekly_view_count mode=WINDOW period=WEEKLY, INCREMENT item_view_count mode=LIFETIME, APPEND_UNIQUE recently_viewed Weekly threshold có thể emit View_10_Items_Weekly Direct Event; item/recent list phục vụ recommend
UC-04 Transfer Balance Transfer_Balance command Direct Flow App -> TP sync API N/A Debit/Credit ACID + noti
UC-05 Inactive Account Login Login Tracking Event -> Generated Direct Event EP -> tracking-topic -> ATTR -> events-topic -> TP SET last_login_date with threshold evaluateOn=BEFORE_UPDATE, TIME_GAP_GT 90D Account_Reactivated Direct Event -> welcome back offer/noti
UC-06 Welcome Offer Welcome_Offer Direct Event External/Card System -> EP -> events-topic -> TP N/A +100đ, ONCE_LIFETIME by TP idempotency
UC-07 Update Profile Profile_Completed Direct Event Profile Service -> EP -> events-topic -> TP N/A +100đ nếu hoàn tất trước deadline
UC-08 Survey Survey_Completed Tracking Event -> Generated Direct Event EP -> tracking-topic -> ATTR -> events-topic -> TP SET_TRUE_ONCE survey_completed:{surveyId} Survey_Completed_Reward Direct Event; chỉ reward khi set flag thành công
UC-09 Redeem Item Redeem_Item command Direct Flow App -> TP sync API N/A Trừ điểm + reserve inventory ACID; TP publish noti và tracking event sau commit
UC-10 Merchant Scan Scan_Voucher command Direct Flow Merchant App -> TP sync API N/A Voucher USED ACID + noti
UC-11 Password Expiring Password_Expiring Notification Event Marketing -> EP/noti-topic -> NS N/A Push/Email cảnh báo
UC-12 Most Redeem Item Item_Redeemed_Count Tracking Event TP outbox -> tracking-topic -> ATTR INCREMENT total_redeem_count mode=LIFETIME on Item Cập nhật ranking/reporting
UC-13 Dormant User Dormant_User Direct Event Marketing -> EP -> events-topic -> TP N/A Cấp ưu đãi riêng + noti

10. Chi tiết Các Use Case Quan trọng

10.1. UC-08 Survey Reward Gate

UC-08 không nên dual publish trực tiếp tới cả TP và Attribute, vì có nguy cơ cấp voucher trước khi biết survey flag đã được set thành công.

Luồng chuẩn:

sequenceDiagram
    participant APP as Member App
    participant EP as Event Processor
    participant TT as tracking-topic
    participant ATTR as Attribute Service
    participant ET as events-topic
    participant TP as Transaction Service

    APP->>EP: Survey_Completed
    EP->>TT: tracking message
    TT->>ATTR: Consume
    ATTR->>ATTR: SET_TRUE_ONCE survey_completed:{surveyId}
    alt first completion
        ATTR->>ET: Generated Direct Event: Survey_Completed_Reward
        ET->>TP: Consume Direct Event and grant voucher
    else already completed
        ATTR->>ATTR: No-op, no reward
    end

10.2. UC-12 Redeem Counter

Redeem là Direct Flow, không đi qua Event Processor cho primary transaction.

Luồng chuẩn:

  1. Member App gọi TP API Redeem_Item.
  2. TP validate balance, inventory, voucher rule trong DB transaction.
  3. TP commit thành công.
  4. TP outbox publish:
    • Notification event vào noti-topic.
    • Item_Redeemed_Count vào tracking-topic.
  5. Attribute tăng Item:{itemId}:total_redeem_count.

Nếu redeem fail, không publish tracking event.


11. Data Model Khuyến nghị

11.1. Attribute Record

attribute_record
- entity_type       -- Customer, Item, Voucher...
- entity_id
- attribute_key
- value_json
- value_type
- last_event_id
- last_event_time
- created_at
- updated_at

unique(entity_type, entity_id, attribute_key)

11.2. Processed Event

processed_event
- service_name
- event_id
- idempotency_key
- processed_at
- status

unique(service_name, event_id)
unique(service_name, idempotency_key) where idempotency_key is not null

11.3. Outbox

outbox_event
- id
- aggregate_type
- aggregate_id
- event_name
- event_payload
- target_topic
- idempotency_key
- status
- retry_count
- next_retry_at
- created_at
- published_at

12. Reliability, DLQ & Replay

  • Consumer retry có giới hạn, ví dụ 3 lần nhanh + backoff.
  • Sau khi retry hết, event vào DLQ tương ứng: events-dlq, tracking-dlq, noti-dlq.
  • DLQ payload phải giữ nguyên envelope, error code, stack trace tóm tắt, service name và failed timestamp.
  • Replay DLQ phải đi qua cùng idempotency guard như event bình thường.
  • Monitoring tối thiểu:
    • consumer lag theo topic/consumer group;
    • outbox pending count;
    • DLQ rate;
    • reward duplicate/conflict count;
    • notification send failure rate.

13. Security & Governance

  • Client không được gửi instruction/rule trong event. Instruction chỉ lấy từ registry server-side.
  • Event Processor validate schema, source, authentication/authorization và schema version.
  • TP không tin tuyệt đối Generated Direct Event; TP vẫn check transaction code, campaign rule, eligibility và idempotency trước khi cấp reward.
  • Các field nhạy cảm trong event payload cần được mask/encrypt theo chuẩn bảo mật hiện hành.
  • Registry thay đổi phải có audit log: ai sửa, sửa gì, effective time, rollback version.

14. Kết luận Thiết kế

Thiết kế v2 chốt các điểm chính:

  • Direct Flow là sync API vào TP cho nghiệp vụ cần phản hồi ngay và ACID.
  • Direct Event là async Kafka event vào events-topic cho reward/rule không cần sync response.
  • Tracking/counter/state nằm ở Attribute, không đẩy counter tần suất cao vào TP.
  • Khi Tracking Event đạt threshold, Attribute sinh một Generated Direct Event mới vào events-topic; TP consume event này như Direct Event async bình thường.
  • UC-08 dùng Attribute làm gate để tránh cấp voucher trùng.
  • UC-12 lấy event sau transaction thành công từ TP outbox, không làm ảnh hưởng luồng redeem sync.
  • Operation catalog phase 1 chỉ gồm SET, SET_TRUE_ONCE, INCREMENT, APPEND_UNIQUE; các biến thể streak/window/timer là mode hoặc threshold, không phải operation riêng.
  • Trigger policy chỉ cần ONCE_LIFETIMEONCE_PER_PERIOD cho giai đoạn hiện tại.