总账的聚合设计

Aggregate Design for Ledger

我正在尝试使用 DDD 设计一个复式记账分类账,运行 在定义聚合根时遇到了一些麻烦。共有三个域模型:

  1. LedgerLine:包含数量、创建时间戳等数据的单个订单项
  2. LedgerEntry:记入账本。每个条目包含多个 LedgerLine,其中借方和贷方行必须平衡。
  3. LedgerAccount: 账本中的账户。有两种类型的账户:(1) 内部账户(例如现金)(2) 外部账户(例如关联的银行账户)。外部帐户可以是 added/removed.

在网上阅读了一些文章后(例如这篇:https://lorenzo-dee.blogspot.com/2013/06/domain-driven-design-accounting-domain.html?m=0)。看起来 LedgerEntry 应该是一个聚合根,包含对 LedgerLine 的引用。 LedgerAccount 应该是另一个聚合根。 LedgerLines 将保存相应的 LedgerAccount 的 ID。

虽然这很有意义,但我无法弄清楚在添加分类帐行时如何更新分类帐帐户的余额。上面的文章建议应该动态计算余额,这意味着添加 LedgerEntrys 时不需要更新它。但是,我将 Amazon QLDB 用于分类帐,他们的解决方案工程师特别建议应计算余额并将其存储在 LedgerAccount 上,因为 QLDB 并未针对此类“扫描大量文档”操作进行优化.

现在问题来了:

  1. 如果我在添加 LedgerEntry 时同步更新 balance 字段,那么我将在一次操作中更新两个聚合,这违反了一致性边界。
  2. 如果我在收到“添加 LedgerEntry”操作发出的事件后异步更新 balance 字段,那么如果我添加另一个 LedgerEntry 花费账户余额,这可能导致透支。
  3. 如果我将 LedgerAccount 模型归入 LedgerEntry 的同一集合中,那么我将失去 add/remove 个人 LedgerAccount 的能力,因为我无法查询它们直接。
  4. 如果我去掉 balance 字段并即时计算它,那么可能会出现性能问题,因为 (1) QLDB 限制 (2) 分类账行数是无限的。

那么这里的正确设计是什么?非常感谢任何帮助!

您可以使用 Saga 模式来确保整个过程完成或失败。

这是一本入门书...https://medium.com/@lfgcampos/saga-pattern-8394e29bbb85

  • 我会将 'reserved funds' 拥有的 collection 添加到分类帐帐户。
  • 分类帐将有 'Actual' 余额和 'Available Balance'。
  • 'Available Balance' 是 'Actual' 余额减去 'reserved funds'
  • 的值

使用 Saga 管理流程:

  1. 尝试在帐户总计上保留资金。 Ledger Account 将检查其可用余额(实际减去保留资金总额),如果足够,则将另一笔保留资金添加到其 collection。如果预订成功,帐户聚合将 return 一个预订唯一 ID。如果预订失败,则无法发布条目。

  2. 尝试完成复式记账。如果失败,请向引用预订唯一 ID 的帐户聚合发送 'release reservation' 命令,这将删除预订,我们回到开始的地方。

  3. 复式记账完成后,向'complete' reservation Account 发送命令,其中包含reservation unique id。然后,帐户聚合将删除预留并调整其实际余额​​。

通过这种方式,您可以管理分布式事务,而不会透支账户。

聚合根应该作为事务边界。多方交易跨越多个账户,因此一个账户不能。

所以账本本身就是一个聚合根。会计事务应对应数据库事务。

实际上,“账本本身”并不意味着单例。可以是组织分部*时间段分类账。它通常在非计算机事件源系统中。

更新。

分类帐帐户余额只是分类帐的一个视图。作为一种观点,它具有某个已知事件的状态。在决定是否接受操作时,您应该确保分类帐的实际状态是截至余额的最新处理状态。如果不是 - 应先处理较新的事件,然后再尝试帐户操作。