事件溯源模式:分離事件的發生和捕獲兩種不同時間 - verraes

banq發表於2022-03-23

領域事件中,使用單獨的時間戳來區分事件的發生時間和捕獲時間。
 

問題
一個領域事件通常有一個時間戳。一個常見的模式是讓eventstore在事件被寫入時新增時間戳。
例如,可以有一個名為record_at的資料庫欄位,其值預設為now()。該欄位被認為是事件後設資料的一部分(而不是特定領域的有效載荷)。該欄位可以在消費該事件的使用者區程式碼中訪問,並可用於基礎設施任務(如按時間順序排列事件)或特定領域的操作、預測、分析...。

這在很多情況下是沒有問題的。對於大多數目的,記錄的時間與事件發生的時間相吻合。許多事件是由系統產生的,或者是使用者產生的行為的直接後果。即使在使用者點選一個按鈕和由此產生的事件被持久化之間有一個小的延遲,其差異也往往可以忽略不計。
畢竟,很多業務流程都是以幾分鐘、幾小時、幾天、甚至幾個月和幾個季度為單位進行操作的。

然而,有時我們實際上關心的是其中的差別。假設每天午夜我們都會收到一份包含當天銀行交易的報表。我們將每筆交易記錄為一個域事件,它的record_at時間戳是在午夜之後。然而,該交易是在前一天的某個地方發生的。如果支付日期對利息的計算、財政利益、法律影響或其他時間敏感方面有影響,這就很重要。

另一個例子是在一個車隊管理系統中跟蹤車禍。當我們持續記錄事件的時候,我們有記錄的時間。但車禍是什麼時候發生的,什麼時候被報告的?
 

解決方案
識別領域事件型別,在這些型別中,事件發生的時間與記錄的時間是不同的,並且這種差異是重要的。在領域事件的模式中,新增一個反映事件發生時間的領域特定屬性。用它在領域中的用途來命名該屬性。不要有一個預設的now()值,而是依靠事件生產者來填寫這個屬性。

事件的消費者現在可以使用該屬性來做出相關的決定。

 

案例程式碼
在我們的銀行對賬單示例中,架構可能如下所示:

# (pseudocode, details omitted)
- Event
  - eventId: UUID
  - recorded_at: timestamp
  - type: BankAccountWasCredited
  - payload:
    - amount: number
    - currency: string
    - deposited_at: timestamp  


deposited_at屬性表示實際事務時間,它發生在recorded_at持久時間之前。該事件是多時間的,因為它代表時間的兩個時刻。

為什麼我們不簡單地在事務發生的地方將事件注入到事件庫中的某個地方呢?這將簡化事件本身的設計。
但這其實是個壞主意。
事件儲存庫是按時間順序排列的,所以我們必須插入事件並轉移所有後面的事件。更重要的是,在事件源中,事件是在它們被持久化後釋出的。如果我們在歷史的某個地方注入事件,我們就必須告訴所有的消費者,這個特別的事件是不符合順序的。這給消費者帶來了額外的複雜性。
 
良好的習慣
即使對於沒有特殊時間戳要求的事件,我也推薦這種技術。這意味著每個事件型別的有效載荷將至少有1個時間戳。

- Event
  - eventId: UUID
  - recorded_at: timestamp
  - type: BankAccountWasCredited
  - payload:
    - amount: number
    - currency: string
    - deposited_at: timestamp
    - statement_received_at: timestamp

請注意,新的 statement_received_at 包含重複的資訊,因為時間戳將等於 recorded_at 的時間戳。
在我看來,與這些好處相比,這只是一個很小的代價。
  • 我們現在為該屬性起了一個特定領域的名字,這更好地傳達了設計的意圖。另一個開發者可以理解該屬性在域中的含義,而不必猜測他們是否可以使用record_at欄位來滿足他們的需求。
  • 我們有更好的解耦:消費者只需要擔心有效載荷,而不需要依賴後設資料。這是有道理的,因為它將基礎設施的需求與特定領域的需求解耦。由於不依賴後設資料,你可以(在理論上)用不同的供應商替換事件儲存,而不影響領域模型。
  • 模型本身的演進也更容易。如果我們依賴於record_at,而後來我們決定需要跟蹤發生,我們就需要調整所有消費者的程式碼。如果我們已經有了特定領域的屬性,我們只需要改變生產者。讓我們看一個例子。

更多點選標題

相關文章