<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Aggregate-Root on Tarragon</title><link>https://tarrragon.github.io/blog/tags/aggregate-root/</link><description>Recent content in Aggregate-Root on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 20 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/aggregate-root/index.xml" rel="self" type="application/rss+xml"/><item><title>Aggregate Root</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/aggregate-root/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/aggregate-root/</guid><description>&lt;p>聚合根是對外代表一組資料一致性的邊界物件。外部操作只跟聚合根互動、不直接碰內部的 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/entity/" data-link-title="Entity" data-link-desc="判斷一個概念該建成 entity 還是 value object 時使用。entity 的同一性由身份定義——欄位全部改變、只要身份參照不變就是同一個。">entity&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/value-object/" data-link-title="Value Object" data-link-desc="判斷一個概念該用內容比對還是身份追蹤時使用。value object 的同一性由內容定義——內容相等就是同一個、替換實例對系統沒有影響。">value object&lt;/a>；聚合根負責保證邊界內所有&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">不變式&lt;/a>在每次操作結束時成立。一致性的範圍就是聚合的邊界——邊界內是同一個事務、邊界外是最終一致。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>聚合根是本模組尚未展開的章節主題（模組 backlog）。目前在教學章中以例子形態出現：一個 POS 專案的商品從扁平結構長成雙層聚合——商品作為聚合根、規格作為子層——欄位歸屬的判準是「兩個規格會不會不同」（&lt;a href="https://tarrragon.github.io/blog/ddd/data-bag-vs-domain-model/" data-link-title="資料袋與領域模型" data-link-desc="判斷一個型別該是一袋欄位還是有行為的領域模型：判準是「有沒有不允許任意組合的欄位」。含判準用錯時規則退化成建議的機制、以及資料袋起步後升級的演化訊號。">資料袋與領域模型&lt;/a>）。聚合的邊界跟 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/entity/" data-link-title="Entity" data-link-desc="判斷一個概念該建成 entity 還是 value object 時使用。entity 的同一性由身份定義——欄位全部改變、只要身份參照不變就是同一個。">entity&lt;/a> 的生命週期邊界是兩個獨立的設計決策。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>多個物件的修改必須一起成功或一起失敗、而這些物件目前散在不同的存取路徑——缺少聚合根把它們收在同一個一致性邊界裡。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>聚合根的邊界劃太大會把不相關的物件鎖在同一個事務、限制併發；劃太小會讓跨聚合的一致性問題推到應用層。邊界的判準是「哪些不變式必須在同一次操作內成立」。完整教學等 case 累積後展開。&lt;/p></description><content:encoded><![CDATA[<p>聚合根是對外代表一組資料一致性的邊界物件。外部操作只跟聚合根互動、不直接碰內部的 <a href="/blog/ddd/knowledge-cards/entity/" data-link-title="Entity" data-link-desc="判斷一個概念該建成 entity 還是 value object 時使用。entity 的同一性由身份定義——欄位全部改變、只要身份參照不變就是同一個。">entity</a> 或 <a href="/blog/ddd/knowledge-cards/value-object/" data-link-title="Value Object" data-link-desc="判斷一個概念該用內容比對還是身份追蹤時使用。value object 的同一性由內容定義——內容相等就是同一個、替換實例對系統沒有影響。">value object</a>；聚合根負責保證邊界內所有<a href="/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">不變式</a>在每次操作結束時成立。一致性的範圍就是聚合的邊界——邊界內是同一個事務、邊界外是最終一致。</p>
<h2 id="概念位置">概念位置</h2>
<p>聚合根是本模組尚未展開的章節主題（模組 backlog）。目前在教學章中以例子形態出現：一個 POS 專案的商品從扁平結構長成雙層聚合——商品作為聚合根、規格作為子層——欄位歸屬的判準是「兩個規格會不會不同」（<a href="/blog/ddd/data-bag-vs-domain-model/" data-link-title="資料袋與領域模型" data-link-desc="判斷一個型別該是一袋欄位還是有行為的領域模型：判準是「有沒有不允許任意組合的欄位」。含判準用錯時規則退化成建議的機制、以及資料袋起步後升級的演化訊號。">資料袋與領域模型</a>）。聚合的邊界跟 <a href="/blog/ddd/knowledge-cards/entity/" data-link-title="Entity" data-link-desc="判斷一個概念該建成 entity 還是 value object 時使用。entity 的同一性由身份定義——欄位全部改變、只要身份參照不變就是同一個。">entity</a> 的生命週期邊界是兩個獨立的設計決策。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>多個物件的修改必須一起成功或一起失敗、而這些物件目前散在不同的存取路徑——缺少聚合根把它們收在同一個一致性邊界裡。</p>
<h2 id="設計責任">設計責任</h2>
<p>聚合根的邊界劃太大會把不相關的物件鎖在同一個事務、限制併發；劃太小會讓跨聚合的一致性問題推到應用層。邊界的判準是「哪些不變式必須在同一次操作內成立」。完整教學等 case 累積後展開。</p>
]]></content:encoded></item><item><title>Repository</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/repository/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/repository/</guid><description>&lt;p>repository 把「怎麼存、怎麼查」包裝成領域語言的介面：呼叫端看到的是存書、查書這類操作，看不到底層是資料庫、檔案還是記憶體。它是一種 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port&lt;/a>——依賴方向朝內、簽名只用領域型別——差別在 repository 專職 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate root&lt;/a> 的存取。repository 回傳的形狀是 aggregate 的形狀（entity 或 entity 集合），這條界線是它跟 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="查詢該由 repository 回 aggregate、還是該有自己的查詢側模型時使用。read model 是為讀需求的形狀而建的模型——回答「畫面需要什麼形狀」、與 aggregate 的形狀分離。">read model&lt;/a> 分工的起點。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>repository 預設是 pull 介面：呼叫端主動問「現在的資料長怎樣」，一次拿到一份 aggregate 形狀的答案。它不天生具備「資料變了通知我」的推送能力——這條能力屬於 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/observation-outlet/" data-link-title="Observation Outlet（觀測出口）" data-link-desc="repository 只有 pull 介面、衍生視圖靠補償刷新，考慮補「資料變了」的推送能力時使用。觀測出口是 pull 介面的 push 對應——能力橫跨三層、歸屬由各層的表達語言決定。">observation outlet&lt;/a>，是 repository 介面之上的另一層職責，需要另外設計才會出現。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>repository 介面開始長出 &lt;code>getMonthlyStatistics()&lt;/code>、&lt;code>searchWithPagination()&lt;/code> 這類回傳統計值或反正規化形狀的方法，是讀的形狀混進 aggregate 介面的訊號——查詢集合已經大到值得思考該不該抽獨立的讀側介面，量測與升級路徑見 &lt;a href="https://tarrragon.github.io/blog/ddd/read-model-upgrade-signals/" data-link-title="讀模型的升級判準" data-link-desc="repository 開始長出畫面專用查詢方法、或有人提議「上 CQRS」時使用。讀側是一道階梯而不是開關：訊號決定該爬到哪一階，自檢問句是「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。">讀模型的升級判準&lt;/a>。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>repository 的設計責任是守住 aggregate 一致性邊界的存取入口，不是最佳化每一種讀需求的效能與形狀——後者是 read model 的責任。repository 是否該同時扛下變更通知，判準不是「需求來自誰」而是介面用什麼語言表達，完整推導見 &lt;a href="https://tarrragon.github.io/blog/ddd/observation-outlet-responsibility-split/" data-link-title="觀測出口的職責三分" data-link-desc="repository 要補「資料變了」的推送能力、卻不確定 Stream 介面放 domain 算不算洩漏時使用。歸屬判準是介面用什麼語言表達、不是需求來自誰：契約歸 domain、變更偵測歸 infrastructure、框架訂閱歸組裝層。">觀測出口的職責三分&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>repository 把「怎麼存、怎麼查」包裝成領域語言的介面：呼叫端看到的是存書、查書這類操作，看不到底層是資料庫、檔案還是記憶體。它是一種 <a href="/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port</a>——依賴方向朝內、簽名只用領域型別——差別在 repository 專職 <a href="/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate root</a> 的存取。repository 回傳的形狀是 aggregate 的形狀（entity 或 entity 集合），這條界線是它跟 <a href="/blog/ddd/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="查詢該由 repository 回 aggregate、還是該有自己的查詢側模型時使用。read model 是為讀需求的形狀而建的模型——回答「畫面需要什麼形狀」、與 aggregate 的形狀分離。">read model</a> 分工的起點。</p>
<h2 id="概念位置">概念位置</h2>
<p>repository 預設是 pull 介面：呼叫端主動問「現在的資料長怎樣」，一次拿到一份 aggregate 形狀的答案。它不天生具備「資料變了通知我」的推送能力——這條能力屬於 <a href="/blog/ddd/knowledge-cards/observation-outlet/" data-link-title="Observation Outlet（觀測出口）" data-link-desc="repository 只有 pull 介面、衍生視圖靠補償刷新，考慮補「資料變了」的推送能力時使用。觀測出口是 pull 介面的 push 對應——能力橫跨三層、歸屬由各層的表達語言決定。">observation outlet</a>，是 repository 介面之上的另一層職責，需要另外設計才會出現。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>repository 介面開始長出 <code>getMonthlyStatistics()</code>、<code>searchWithPagination()</code> 這類回傳統計值或反正規化形狀的方法，是讀的形狀混進 aggregate 介面的訊號——查詢集合已經大到值得思考該不該抽獨立的讀側介面，量測與升級路徑見 <a href="/blog/ddd/read-model-upgrade-signals/" data-link-title="讀模型的升級判準" data-link-desc="repository 開始長出畫面專用查詢方法、或有人提議「上 CQRS」時使用。讀側是一道階梯而不是開關：訊號決定該爬到哪一階，自檢問句是「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。">讀模型的升級判準</a>。</p>
<h2 id="設計責任">設計責任</h2>
<p>repository 的設計責任是守住 aggregate 一致性邊界的存取入口，不是最佳化每一種讀需求的效能與形狀——後者是 read model 的責任。repository 是否該同時扛下變更通知，判準不是「需求來自誰」而是介面用什麼語言表達，完整推導見 <a href="/blog/ddd/observation-outlet-responsibility-split/" data-link-title="觀測出口的職責三分" data-link-desc="repository 要補「資料變了」的推送能力、卻不確定 Stream 介面放 domain 算不算洩漏時使用。歸屬判準是介面用什麼語言表達、不是需求來自誰：契約歸 domain、變更偵測歸 infrastructure、框架訂閱歸組裝層。">觀測出口的職責三分</a>。</p>
]]></content:encoded></item><item><title>Bounded Context</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/bounded-context/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/bounded-context/</guid><description>&lt;p>bounded context 是一個模型與詞彙維持一致的邊界：邊界內，同一個詞（例如「訂單」）只有一種意義、同一套業務規則統一適用；跨過邊界，同一個詞可能換了意義、規則跟著換。&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate root&lt;/a> 守的一致性邊界在單一 aggregate 內；bounded context 守的是更大一層——一整套模型、詞彙與規則在哪裡開始、哪裡結束。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>單一 bounded context 內的架構判準，跨過邊界不必然照搬。讀模型升級的四階梯（消費端投影 → 抽讀 port → 專用投影 → 事件同步的獨立儲存）假設操作發生在單一 bounded context 內；報表服務訂閱多個 domain 的事件建置共享視圖，是跨 bounded context 的讀模型，這時引入的關注點——跨服務事件契約穩定性、schema 演進、跨信任邊界的最終一致性——不是「多買一種能力」能概括，完整推導見 &lt;a href="https://tarrragon.github.io/blog/ddd/read-model-upgrade-signals/" data-link-title="讀模型的升級判準" data-link-desc="repository 開始長出畫面專用查詢方法、或有人提議「上 CQRS」時使用。讀側是一道階梯而不是開關：訊號決定該爬到哪一階，自檢問句是「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。">讀模型的升級判準&lt;/a>。這類跨邊界溝通的常見載體是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event&lt;/a>，其中攜帶足量狀態給下游的作法見 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/event-carried-state-transfer/" data-link-title="Event-Carried State Transfer" data-link-desc="跨服務的 domain event payload 開始塞進足量當前狀態、要判斷這是正當設計還是載體誤用時使用。event-carried state transfer 是刻意讓事件攜帶足量狀態、讓下游服務不必回頭查詢來源的設計。">event-carried state transfer&lt;/a>。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>一個判準句在單一服務內成立、換到跨服務場景就開始要補額外的機制（契約版本、schema 遷移、補償流程），是踩到 bounded context 邊界的訊號——原本的判準沒有錯，是它的適用範圍被劃在邊界內，邊界外要重新論證。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>bounded context 的設計責任是替模型畫出「在哪裡可以直接假設一致」的邊界，邊界外的互動改走顯式的契約與轉換，不能沿用邊界內的內部語意。這條邊界也是判斷同一套架構結論能不能套用的分界——邊界內成立的推導，邊界外要重新論證，不是預設繼續有效。&lt;/p></description><content:encoded><![CDATA[<p>bounded context 是一個模型與詞彙維持一致的邊界：邊界內，同一個詞（例如「訂單」）只有一種意義、同一套業務規則統一適用；跨過邊界，同一個詞可能換了意義、規則跟著換。<a href="/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate root</a> 守的一致性邊界在單一 aggregate 內；bounded context 守的是更大一層——一整套模型、詞彙與規則在哪裡開始、哪裡結束。</p>
<h2 id="概念位置">概念位置</h2>
<p>單一 bounded context 內的架構判準，跨過邊界不必然照搬。讀模型升級的四階梯（消費端投影 → 抽讀 port → 專用投影 → 事件同步的獨立儲存）假設操作發生在單一 bounded context 內；報表服務訂閱多個 domain 的事件建置共享視圖，是跨 bounded context 的讀模型，這時引入的關注點——跨服務事件契約穩定性、schema 演進、跨信任邊界的最終一致性——不是「多買一種能力」能概括，完整推導見 <a href="/blog/ddd/read-model-upgrade-signals/" data-link-title="讀模型的升級判準" data-link-desc="repository 開始長出畫面專用查詢方法、或有人提議「上 CQRS」時使用。讀側是一道階梯而不是開關：訊號決定該爬到哪一階，自檢問句是「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。">讀模型的升級判準</a>。這類跨邊界溝通的常見載體是 <a href="/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event</a>，其中攜帶足量狀態給下游的作法見 <a href="/blog/ddd/knowledge-cards/event-carried-state-transfer/" data-link-title="Event-Carried State Transfer" data-link-desc="跨服務的 domain event payload 開始塞進足量當前狀態、要判斷這是正當設計還是載體誤用時使用。event-carried state transfer 是刻意讓事件攜帶足量狀態、讓下游服務不必回頭查詢來源的設計。">event-carried state transfer</a>。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>一個判準句在單一服務內成立、換到跨服務場景就開始要補額外的機制（契約版本、schema 遷移、補償流程），是踩到 bounded context 邊界的訊號——原本的判準沒有錯，是它的適用範圍被劃在邊界內，邊界外要重新論證。</p>
<h2 id="設計責任">設計責任</h2>
<p>bounded context 的設計責任是替模型畫出「在哪裡可以直接假設一致」的邊界，邊界外的互動改走顯式的契約與轉換，不能沿用邊界內的內部語意。這條邊界也是判斷同一套架構結論能不能套用的分界——邊界內成立的推導，邊界外要重新論證，不是預設繼續有效。</p>
]]></content:encoded></item></channel></rss>