<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>DDD on 追风笔记</title><link>https://88ok.github.io/tags/ddd/</link><description>Recent content in DDD on 追风笔记</description><generator>Hugo</generator><language>zh</language><lastBuildDate>Wed, 12 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://88ok.github.io/tags/ddd/index.xml" rel="self" type="application/rss+xml"/><item><title>聚合根与领域事件：DDD 战术建模</title><link>https://88ok.github.io/columns/architecture/ddd-aggregates/</link><pubDate>Sun, 15 Mar 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/columns/architecture/ddd-aggregates/</guid><description>&lt;p&gt;银行核心系统里最容易被低估的概念，是「聚合」。很多人把 DDD 战术设计理解成「给实体加几个注解」，结果聚合越写越大，事务越来越长，最后把数据库和团队都拖垮。本文把聚合根和领域事件放回它们本来的位置：一致性边界的设计工具，以及领域向外界发声的窗口。&lt;/p&gt;&#10;&lt;h2 id="为什么需要聚合"&gt;为什么需要聚合&#10;&lt;/h2&gt;&#10;&lt;p&gt;领域模型不是一张 ER 图。ER 图关心「数据怎么存」，聚合关心「哪些数据必须在同一个事务里保持一致」。这两件事经常错位，而错位的代价在金融场景里格外昂贵——账务不平、状态错乱，往往不是因为算法错，而是因为一致性边界划错了。&lt;/p&gt;&#10;&lt;h3 id="一致性边界"&gt;一致性边界&#10;&lt;/h3&gt;&#10;&lt;p&gt;聚合的第一性原理是：&lt;strong&gt;在边界之内，用强一致性保证业务规则不被破坏；在边界之外，用最终一致性传递变化&lt;/strong&gt;。一个账户余额不能为负，这条规则必须在一个事务内被满足，所以它属于聚合内部。而「账户余额变动后通知风控」，则属于边界之外，交给领域事件。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;经验法则：如果两条业务规则必须同时为真，它们大概率属于同一个聚合；如果可以接受短暂的不一致，它们就分属不同聚合。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="聚合不是大对象"&gt;聚合不是「大对象」&#10;&lt;/h3&gt;&#10;&lt;p&gt;最常见的反模式，是把「客户」做成包含地址、联系人、合同、账户、画像的超级对象。它在概念上很完整，但在工程上是个灾难：每次改一个手机号，都要加载半个数据库，还要锁住一堆根本无关的数据。&lt;/p&gt;&#10;&lt;p&gt;聚合的边界应以&lt;strong&gt;事务一致性&lt;/strong&gt;而非&lt;strong&gt;业务概念大小&lt;/strong&gt;来划。概念上属于「同一个东西」的数据，未必需要在同一个事务里。&lt;/p&gt;&#10;&lt;h2 id="聚合根的设计原则"&gt;聚合根的设计原则&#10;&lt;/h2&gt;&#10;&lt;p&gt;聚合根是聚合对外的唯一入口。外部只能持有聚合根的引用，不能直接操作聚合内部的实体或值对象。&lt;/p&gt;&#10;&lt;h3 id="引用靠-id不靠对象"&gt;引用靠 ID，不靠对象&#10;&lt;/h3&gt;&#10;&lt;p&gt;跨聚合的关联，永远用标识符，而不是对象引用。聚合 A 不应直接持有聚合 B 的对象，否则两个聚合会被悄悄绑进同一事务。&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-8481552c-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="20"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-8481552c-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AccountId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 聚合根标识&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ownerId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 跨聚合引用：只存 ID&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Money&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DomainEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pendingEvents&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;withdraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Money&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isLessThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;InsufficientBalanceException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pendingEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MoneyWithdrawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DomainEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;drainEvents&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pendingEvents&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pendingEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="工厂与仓储"&gt;工厂与仓储&#10;&lt;/h3&gt;&#10;&lt;p&gt;复杂聚合的创建交给工厂，避免调用方掌握内部构造细节；聚合的持久化交给仓储，且&lt;strong&gt;仓储只按聚合根 ID 加载整个聚合&lt;/strong&gt;，不存在「只查一半」的仓储方法。&lt;/p&gt;</description></item><item><title>《领域驱动设计》读书笔记</title><link>https://88ok.github.io/blog/reading-notes-ddd/</link><pubDate>Tue, 14 Apr 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/blog/reading-notes-ddd/</guid><description>&lt;p&gt;这本书我读过三遍。第一遍在工作两年时，只记住了实体、值对象、聚合这几个名词；第二遍在做核心重构时，才发现前面几章才是重点；第三遍是带团队时读的，读出来的东西又不一样。&lt;/p&gt;&#10;&lt;h2 id="这本书真正的价值"&gt;这本书真正的价值&#10;&lt;/h2&gt;&#10;&lt;p&gt;大部分人把它当模式手册用，翻到&amp;quot;聚合&amp;quot;那一节抄个定义就开始设计。但 Evans 花了整本书篇幅想说的其实是一件事：&lt;strong&gt;软件设计的瓶颈是知识，不是技术&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;模型不是画出来的，是在和业务专家反复对话中&amp;quot;长&amp;quot;出来的。书里那些模式只是长出来之后用于表达的工具。工具本身不产生知识。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;一个团队如果没有和业务专家持续对话的机制，那么它用不用 DDD 的模式都无所谓——反正模型里没有真正的领域知识。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="我划重点的三处"&gt;我划重点的三处&#10;&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;第一是统一语言。&lt;/strong&gt; 这是全书性价比最高的概念，也最容易被跳过。如果业务方说&amp;quot;客户&amp;quot;指的是签约主体，而代码里的 &lt;code&gt;Customer&lt;/code&gt; 指的是自然人，那么所有后续设计都建立在一个错位的基础上。我们后来的做法很土：维护一份术语表，中英文对照，明确写出每个词的边界和不包含什么。上线一年下来，它减少的沟通成本比任何架构决策都多。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;第二是模型驱动设计的&amp;quot;绑定&amp;quot;要求。&lt;/strong&gt; 书里强调模型和实现必须绑死——模型改了代码就得改，代码改了模型文档也得改。做不到这一点，模型就退化成挂在墙上的装饰。这也是为什么我不再维护单独的模型图，而是让代码本身成为模型的唯一表达：&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-04f985f8-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="10"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-04f985f8-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 反例：技术化命名，业务方看不懂，术语在翻译中丢失&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RuleProcessor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BigDecimal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BigDecimal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BigDecimal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 正例：类名与方法名直接采用统一语言中的业务术语&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// accrual（计提）不简化成 calc，basis（计息基础）不省略&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InterestAccrualRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Money&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;accrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DailyBalance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DayCountBasis&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;basis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;strong&gt;第三是&amp;quot;深层模型&amp;quot;和重构的关系。&lt;/strong&gt; 书里说好模型需要经历多次突破，而突破往往来自某次对业务的重新理解。这一点我深有体会：我们的账务模型是在第三次重写时才想清楚&amp;quot;分录是第一公民、交易只是外壳&amp;quot;，前两版都是围着交易类型打转。第一次就设计出好模型的期待本身就不现实。&lt;/p&gt;&#10;&lt;h2 id="读完之后我改了什么"&gt;读完之后我改了什么&#10;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;需求评审改成建模会&lt;/strong&gt;。不再让业务方念文档，而是当场画概念、当场确认术语。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;术语表进代码仓库&lt;/strong&gt;。放在 &lt;code&gt;docs/glossary.md&lt;/code&gt;，跟代码一起评审，改术语要走 PR。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;不再追求完整套用模式&lt;/strong&gt;。战术模式里我们只常用值对象和聚合，事件溯源、CQRS 一律不用，除非有明确的读写比例失衡问题。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;接受模型会被重写&lt;/strong&gt;。第一版模型的目标是能跑并暴露问题，不是一次做对。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;如果只有两小时读这本书，我建议读第一部分和第三部分的第 8 章（突破），跳过所有模式定义。模式的定义随便搜一下都有，而&amp;quot;如何通过对话获得领域知识&amp;quot;这件事，只有认真读原文才能体会到。&lt;/p&gt;</description></item><item><title>DDD 在银行核心系统的落地实践</title><link>https://88ok.github.io/blog/ddd-banking-core/</link><pubDate>Mon, 12 Jan 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/blog/ddd-banking-core/</guid><description>&lt;p&gt;银行核心系统是一类很特殊的软件：它的业务规则几十年没怎么变，但承载它的代码每隔七八年就要重写一次。我参与过一次核心的部分重构，也旁观过一次彻底失败的重写。DDD 在这两次里都被提过，但只有一次真的起了作用。这篇把我认为有效的部分和纯属自我感动的部分分开写。&lt;/p&gt;&#10;&lt;h2 id="为什么银行核心需要-ddd"&gt;为什么银行核心需要 DDD&#10;&lt;/h2&gt;&#10;&lt;p&gt;老核心的问题从来不是技术栈老。COBOL 写的联机交易照样能扛住每秒几千笔。真正的问题是：业务知识只存在于少数几个人的脑子里，代码里找不到对应的表达。&lt;/p&gt;&#10;&lt;p&gt;一段典型的老代码长这样：把交易码、账户类型、产品编号、机构号混在一个几百行的过程里，用几十个 &lt;code&gt;IF&lt;/code&gt; 分支区分场景。你想知道&amp;quot;活期账户计息到底怎么算&amp;quot;，只能从头读到尾，然后祈祷没漏掉某个补丁分支。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;代码可以重写，业务知识不能。重构核心系统的第一目标不是换语言，而是把散落在分支里的规则重新变成可以被讨论的概念。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;这正是 DDD 唯一真正值得投入的地方：它提供了一套让业务专家和工程师用同一套词汇讨论问题的方法。别的都是附加品。&lt;/p&gt;&#10;&lt;h2 id="战略设计先切限界上下文"&gt;战略设计：先切限界上下文&#10;&lt;/h2&gt;&#10;&lt;p&gt;如果只能做 DDD 的一件事，就做上下文切分。这一步做错，后面所有战术模式都是白费。&lt;/p&gt;&#10;&lt;h3 id="以业务能力而非表结构切分"&gt;以业务能力而非表结构切分&#10;&lt;/h3&gt;&#10;&lt;p&gt;最常见的错误是按数据库表切：账户表归账户服务，交易表归交易服务。结果是每笔转账都要跨三个服务改数据，分布式事务满天飞。&lt;/p&gt;&#10;&lt;p&gt;我们最后落在这几个上下文上：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;客户与关系&lt;/strong&gt;：客户主体、关系人、证件、KYC 状态&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;账户与协议&lt;/strong&gt;：账号、账户属性、签约关系、限额协议&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;交易处理&lt;/strong&gt;：交易受理、要素校验、路由、冲正&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;账务核算&lt;/strong&gt;：分录、余额、总账、日终结转&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;产品参数&lt;/strong&gt;：利率、费率、期限、计息规则&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;关键判据是：&lt;strong&gt;一次业务动作的强一致性要求是否落在同一个上下文内&lt;/strong&gt;。转账的&amp;quot;扣款成功且分录平衡&amp;quot;必须强一致，所以账务核算不能再往下拆；而&amp;quot;转账成功后更新客户活跃度&amp;quot;完全可以最终一致，那就是两个上下文。&lt;/p&gt;&#10;&lt;h3 id="上下文映射的三种常见关系"&gt;上下文映射的三种常见关系&#10;&lt;/h3&gt;&#10;&lt;div class="td-table-scroll td-table-scroll--static"&gt;&#10;&lt;table&gt;&#10; &lt;thead&gt;&#10; &lt;tr&gt;&#10; &lt;th scope="col"&gt;关系类型&lt;/th&gt;&#10; &lt;th scope="col"&gt;银行场景举例&lt;/th&gt;&#10; &lt;th scope="col"&gt;集成方式&lt;/th&gt;&#10; &lt;th scope="col"&gt;注意点&lt;/th&gt;&#10; &lt;/tr&gt;&#10; &lt;/thead&gt;&#10; &lt;tbody&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;共享内核&lt;/td&gt;&#10; &lt;td&gt;账户上下文与账务上下文共用币种、金额模型&lt;/td&gt;&#10; &lt;td&gt;公共 jar，严格版本管理&lt;/td&gt;&#10; &lt;td&gt;只放值对象，禁止放业务逻辑&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;客户方-供应方&lt;/td&gt;&#10; &lt;td&gt;交易处理调用账务记账&lt;/td&gt;&#10; &lt;td&gt;同步 RPC，供应方定契约&lt;/td&gt;&#10; &lt;td&gt;契约变更必须双版本并行&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;防腐层&lt;/td&gt;&#10; &lt;td&gt;新模块访问老核心账户查询&lt;/td&gt;&#10; &lt;td&gt;适配器 + 模型转换&lt;/td&gt;&#10; &lt;td&gt;老模型绝不允许穿透进来&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&#10;&lt;h3 id="一个反例"&gt;一个反例&#10;&lt;/h3&gt;&#10;&lt;p&gt;我们曾经把&amp;quot;限额&amp;quot;单独切成一个上下文。听起来很干净，实际上限额校验必须和交易受理在同一个事务里完成，跨服务调用一下就把联机响应时间从 40ms 拖到 90ms，还引入了新的失败分支。半年后合并回交易处理上下文。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;上下文边界的正确性，用一致性要求和调用频次验证，不用概念优雅度验证。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="战术落地聚合实体与值对象"&gt;战术落地：聚合、实体与值对象&#10;&lt;/h2&gt;&#10;&lt;p&gt;战术模式在核心系统里的价值远低于战略设计，但用对了确实能减少一批低级 bug。&lt;/p&gt;&#10;&lt;h3 id="聚合的粒度取决于一致性边界"&gt;聚合的粒度取决于一致性边界&#10;&lt;/h3&gt;&#10;&lt;p&gt;账务这块我们的聚合是&amp;quot;分录凭证&amp;quot;（Voucher），不是&amp;quot;账户&amp;quot;。一张凭证包含多条分录，借贷必须平衡，这是不可分割的一致性单元。账户余额是凭证记账的结果，通过事件更新。&lt;/p&gt;</description></item><item><title>在银行客户信息系统（ECIF）里落地 DDD 聚合</title><link>https://88ok.github.io/columns/architecture/ddd-aggregate/</link><pubDate>Wed, 12 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/columns/architecture/ddd-aggregate/</guid><description>&lt;p&gt;ECIF 里「客户」的概念极其庞大：个人、对公、同业，各自的属性、关系、生命周期都不同。如果用一个巨大的 &lt;code&gt;Customer&lt;/code&gt; 实体硬扛，代码会迅速腐化。&lt;/p&gt;&#10;&lt;h2 id="用界限上下文切分"&gt;用界限上下文切分&#10;&lt;/h2&gt;&#10;&lt;p&gt;把客户拆成几个界限上下文：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;客户主数据（Party）&lt;/strong&gt;：统一的自然人/机构标识与基础属性。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;客户画像（Profile）&lt;/strong&gt;：风险偏好、营销标签，读写频率高、变化快。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;客户关系（Relationship）&lt;/strong&gt;：持股、担保、集团关系。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="聚合根怎么定"&gt;聚合根怎么定&#10;&lt;/h2&gt;&#10;&lt;p&gt;每个上下文内部再定聚合根。例如 Party 上下文里，&lt;code&gt;Party&lt;/code&gt; 是聚合根，&lt;code&gt;Address&lt;/code&gt;、&lt;code&gt;Contact&lt;/code&gt; 是其值对象，保证一致性边界内不跨聚合调用。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;经验：聚合的边界应以「事务一致性」而非「业务概念大小」来划。ECIF 里最容易犯的错，就是把所有客户信息塞进一个聚合。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;这样设计后，主数据服务稳定，画像服务可以独立迭代，互不影响。&lt;/p&gt;</description></item></channel></rss>