发票的收件人必须是有效的联系人。
所以你需要注意的第一件事 - 如果两个实体是不同聚合的一部分,你不能真正实现“仅当 那个 实体满足规范时才对这个实体应用更改",因为那个实体可能会在您评估规范和执行写入之间发生变化。
换句话说 - 您只能在聚合边界上获得最终一致性。
聚合是它自己状态的权威,但其他一切(例如,命令消息的内容),它几乎必须接受一些外部权威已经检查了数据。
您可以在这里采取几种方法
1) 你可以盲目接受命令中指定的接收者是有效的。
2) 您可以尝试在从不受信任的来源接收它并将其提交到域之间从某个外部权威机构(又名:某个其他聚合的读取模型)验证收件人的有效性型号。
3) 您可以盲目接受所描述的命令,但在确认收件人的有效性之前,将发票视为临时发票。这意味着要在发票上运行第二个命令来证明收件人。
注意 - 从模型的角度来看,这些不同的命令是等价的,但在应用程序层它们不需要是 - 您可以限制对受信任来源的命令的访问(不要这样做公共 api 的一部分,需要仅对受信任的来源可用的授权等)。
方法#3 是最微服务的,因为这两个命令可以在时间上分开——您可以在 CreateInvoice 命令到达时立即接受它,并异步验证接收者。
您会将方法 4) 放在哪里,发票微服务有自己的联系人存储,只要有 ContactCreated 或 ContactDeleted 事件就会更新?那么这两个实体都是相同服务和边界的一部分。现在应该可以让事情保持一致了,对吧?
没有。您已将这两个实体作为同一个服务的一部分,但问题从来不是它们位于不同的服务中,而是它们位于不同的聚合中——这意味着我们可以同时更改实体状态,这意味着我们无法确保它们立即同步。
如果您想要立即保持一致性,您需要一个以不同方式划分界限的模型。
例如,如果发票实体被建模为联系人聚合的一部分,那么聚合可以确保新发票需要有效收件人这一不变性——域模型使用内存中的状态副本来确认当我们加载时接收者是有效的,并且写入记录簿验证记录簿在加载发生后没有更改。
聚合状态的写入是记录簿中的比较和交换;如果某个并发进程使接收者无效,则 CAS 操作将失败。
当然,权衡是任何对联系人聚合的更改也会导致发票失败;对同一个收件人同时编辑不同的发票会消失。
聚合是全有或全无;它们是不可分离的。
现在,一个结果可能是您的发票汇总有一部分必须立即与收件人一致,而另一部分最终一致,甚至不一致,是可以接受的。在这种情况下,您的目标是重构模型。