[{"data":1,"prerenderedAt":660},["ShallowReactive",2],{"blog-ddd-deployment-modules":3},{"id":4,"title":5,"body":6,"category":644,"date":645,"description":116,"extension":646,"meta":647,"navigation":648,"path":649,"seo":650,"series":651,"seriesOrder":652,"stem":653,"tags":654,"__hash__":659},"blog\u002Fblog\u002Fddd-deployment-modules.md","DDD 实战（七）：从领域模型到部署模块",{"type":7,"value":8,"toc":631},"minimark",[9,13,28,31,34,37,45,48,53,56,63,78,84,90,96,99,102,106,109,119,122,141,144,147,164,167,171,174,177,183,186,203,206,223,226,240,243,247,250,253,331,337,340,344,347,367,370,393,396,399,403,406,409,415,418,421,424,427,430,433,437,440,443,449,452,466,469,483,489,495,499,502,505,531,534,537,540,544,547,586,589,593,596,619,622,625],[10,11,5],"h1",{"id":12},"ddd-实战七从领域模型到部署模块",[14,15,16],"blockquote",{},[17,18,19,20,27],"p",{},"推荐 ",[21,22,26],"a",{"href":23,"rel":24},"https:\u002F\u002Fbewild.ai?code=BYZDOTME",[25],"nofollow","BeWild"," 代充 Codex ，工作猛猛提效。",[17,29,30],{},"领域边界确定以后，很多团队会立刻问：那是不是要拆微服务？",[17,32,33],{},"答案通常是：不一定。",[17,35,36],{},"DDD 可以帮助识别微服务边界，但 DDD 不等于微服务。领域边界、代码边界、数据库边界、部署边界、团队边界，是几件相关但不同的事。",[17,38,39,40],{},"Microsoft 的领域分析文档也把领域分析作为微服务设计的重要输入，但它并没有把“发现一个限界上下文”直接等同于“立刻拆一个独立服务”。",[21,41,44],{"href":42,"rel":43},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Farchitecture\u002Fmicroservices\u002Fmodel\u002Fdomain-analysis",[25],"Microsoft: Domain analysis for microservices",[17,46,47],{},"这篇文章讨论的是：协同办公 IM 这种复杂系统，如何从领域模型走向部署模块，以及每一步的 trade off。",[49,50,52],"h2",{"id":51},"_1-五种边界不要混为一谈","1. 五种边界不要混为一谈",[17,54,55],{},"先把几个边界拆开。",[17,57,58,62],{},[59,60,61],"strong",{},"领域边界","：业务模型在哪个上下文内成立。比如账号、组织、关系、群组、消息是不同上下文。",[17,64,65,68,69,73,74,77],{},[59,66,67],{},"代码边界","：代码如何分包、分模块、定义依赖方向。比如 ",[70,71,72],"code",{},"identity"," 包不能直接 import ",[70,75,76],{},"messaging\u002Finternal\u002Fmysql","。",[17,79,80,83],{},[59,81,82],{},"数据边界","：谁拥有哪类数据事实。比如组织上下文拥有成员事实，搜索上下文只拥有索引视图。",[17,85,86,89],{},[59,87,88],{},"部署边界","：哪些模块独立构建、发布、扩容、回滚。比如消息服务可以独立部署，搜索索引服务也可以独立部署。",[17,91,92,95],{},[59,93,94],{},"团队边界","：哪个团队负责哪个上下文。比如开放平台团队负责应用、机器人、事件订阅，IM 团队负责消息主链路。",[17,97,98],{},"这五种边界可以一致，也可以不一致。",[17,100,101],{},"早期系统可以有清晰领域边界和代码边界，但仍然部署成一个单体。成熟系统可以让高吞吐、高变化、高隔离要求的上下文独立部署。不要一上来就把所有边界都强行对齐。",[49,103,105],{"id":104},"_2-第一阶段普通单体","2. 第一阶段：普通单体",[17,107,108],{},"很多系统最早都是普通单体：",[110,111,117],"pre",{"className":112,"code":114,"language":115,"meta":116},[113],"language-text","app\n├── controller\n├── service\n├── dao\n└── model\n","text","",[70,118,114],{"__ignoreMap":116},[17,120,121],{},"它的优点很明确：",[123,124,125,129,132,135,138],"ul",{},[126,127,128],"li",{},"开发快。",[126,130,131],{},"本地调试简单。",[126,133,134],{},"部署简单。",[126,136,137],{},"事务处理容易。",[126,139,140],{},"团队协作成本低。",[17,142,143],{},"如果产品还在探索期，需求变化大，团队规模小，普通单体是合理选择。",[17,145,146],{},"但当协同办公 IM 的能力不断扩展，普通单体会出现明显问题：",[123,148,149,152,155,158,161],{},[126,150,151],{},"所有业务规则堆在 service。",[126,153,154],{},"账号、组织、群组、消息互相读表。",[126,156,157],{},"消息主链路被搜索、通知、开放平台拖慢。",[126,159,160],{},"发布一个审批改动，也要带着消息模块一起发布。",[126,162,163],{},"故障隔离差，一个低优先级模块拖垮核心链路。",[17,165,166],{},"单体的问题不是“它是单体”，而是没有内部边界。",[49,168,170],{"id":169},"_3-第二阶段模块化单体","3. 第二阶段：模块化单体",[17,172,173],{},"模块化单体是 DDD 很推荐的中间形态。",[17,175,176],{},"它仍然是一个进程、一个部署包，但代码按限界上下文组织：",[110,178,181],{"className":179,"code":180,"language":115,"meta":116},[113],"internal\n├── identity\n├── organization\n├── relationship\n├── group\n├── messaging\n├── notification\n├── openplatform\n├── search\n└── governance\n",[70,182,180],{"__ignoreMap":116},[17,184,185],{},"模块化单体的目标是：",[123,187,188,191,194,197,200],{},[126,189,190],{},"先建立业务边界。",[126,192,193],{},"先明确数据所有权。",[126,195,196],{},"先让依赖方向变清楚。",[126,198,199],{},"先让领域事件和接口抽象成型。",[126,201,202],{},"暂时不承担分布式系统成本。",[17,204,205],{},"它的优点：",[123,207,208,211,214,217,220],{},[126,209,210],{},"比普通单体更可维护。",[126,212,213],{},"比微服务更容易调试和部署。",[126,215,216],{},"可以使用本地事务。",[126,218,219],{},"适合在业务边界还不稳定时快速调整。",[126,221,222],{},"为后续拆服务保留路径。",[17,224,225],{},"它的缺点：",[123,227,228,231,234,237],{},[126,229,230],{},"运行时仍然共享进程。",[126,232,233],{},"故障隔离有限。",[126,235,236],{},"不同模块不能独立扩容。",[126,238,239],{},"如果团队纪律差，仍然可能互相 import 内部实现。",[17,241,242],{},"模块化单体的关键，不是目录拆开，而是依赖规则要立住。比如 Messaging 可以调用 Group 暴露的接口校验会话权限，但不能直接查 Group 的表，也不能 import Group 的基础设施实现。",[49,244,246],{"id":245},"_4-第三阶段选择性拆服务","4. 第三阶段：选择性拆服务",[17,248,249],{},"当业务发展到一定规模，某些上下文会出现独立部署的需求。",[17,251,252],{},"协同办公 IM 中，通常优先考虑拆出的模块包括：",[254,255,256,272],"table",{},[257,258,259],"thead",{},[260,261,262,266,269],"tr",{},[263,264,265],"th",{},"上下文",[263,267,268],{},"拆分原因",[263,270,271],{},"拆分风险",[273,274,275,287,298,309,320],"tbody",{},[260,276,277,281,284],{},[278,279,280],"td",{},"Messaging",[278,282,283],{},"高频核心链路，需要独立扩容和故障隔离",[278,285,286],{},"对组织、群组、风控依赖处理不好会拖慢发送",[260,288,289,292,295],{},[278,290,291],{},"Notification",[278,293,294],{},"推送、提醒、红点适合异步处理",[278,296,297],{},"未读数和消息状态边界容易混淆",[260,299,300,303,306],{},[278,301,302],{},"Search",[278,304,305],{},"索引构建和查询负载特殊",[278,307,308],{},"权限过滤和索引一致性复杂",[260,310,311,314,317],{},[278,312,313],{},"Open Platform",[278,315,316],{},"外部应用、机器人、Webhook 有独立流量和安全模型",[278,318,319],{},"外部 API 版本和内部模型容易互相污染",[260,321,322,325,328],{},[278,323,324],{},"Governance",[278,326,327],{},"风控、审计、合规横跨多个域",[278,329,330],{},"容易变成横向侵入所有模块",[17,332,333,334],{},"选择性拆服务的原则是：",[59,335,336],{},"先拆变化率、负载特征、故障隔离需求明显不同的上下文。",[17,338,339],{},"不要因为领域图上有十个上下文，就一次拆十个微服务。那通常会把业务复杂度变成分布式复杂度。",[49,341,343],{"id":342},"_5-微服务的收益和代价","5. 微服务的收益和代价",[17,345,346],{},"微服务的收益主要有：",[123,348,349,352,355,358,361,364],{},[126,350,351],{},"独立发布。",[126,353,354],{},"独立扩容。",[126,356,357],{},"故障隔离。",[126,359,360],{},"技术栈隔离。",[126,362,363],{},"团队自治。",[126,365,366],{},"数据所有权更容易落实。",[17,368,369],{},"但代价也很重：",[123,371,372,375,378,381,384,387,390],{},[126,373,374],{},"本地调试变复杂。",[126,376,377],{},"调用链变长。",[126,379,380],{},"数据一致性从本地事务变成分布式协作。",[126,382,383],{},"灰度、监控、链路追踪、告警都要跟上。",[126,385,386],{},"接口版本管理变重要。",[126,388,389],{},"测试成本上升。",[126,391,392],{},"小团队容易被运维复杂度拖垮。",[17,394,395],{},"协同办公 IM 特别容易掉进“过早微服务”的坑。比如账号、组织、关系、群组、消息一开始就拆成五个服务，看起来边界很清晰，但每次发消息都要同步调用多个服务。只要任何一个服务抖动，消息链路就变慢。",[17,397,398],{},"这时你会被迫引入缓存、事件、降级、幂等、补偿、链路追踪。结果业务还没复杂到那个程度，基础设施先复杂了。",[49,400,402],{"id":401},"_6-从模块化单体到微服务的迁移路径","6. 从模块化单体到微服务的迁移路径",[17,404,405],{},"比较稳妥的路径是：",[17,407,408],{},"第一步，在单体内按领域上下文重组代码。",[110,410,413],{"className":411,"code":412,"language":115,"meta":116},[113],"messaging\n├── domain\n├── application\n├── adapter\n└── infrastructure\n",[70,414,412],{"__ignoreMap":116},[17,416,417],{},"第二步，禁止跨模块直接访问数据库。所有跨上下文访问都走接口或事件。",[17,419,420],{},"第三步，把领域事件先在进程内跑通，事件模型稳定后再接 MQ。",[17,422,423],{},"第四步，把搜索、通知、开放平台这类天然异步上下文拆成独立进程。",[17,425,426],{},"第五步，再拆消息主链路或组织上下文这类核心服务。",[17,428,429],{},"第六步，根据团队和流量继续拆分。",[17,431,432],{},"这条路径的好处是每一步都能带来收益，而不是为了“微服务化”一次性重写系统。",[49,434,436],{"id":435},"_7-monorepo-适合什么阶段","7. Monorepo 适合什么阶段",[17,438,439],{},"monorepo 不是微服务的反义词。它只是代码仓库组织方式。",[17,441,442],{},"协同办公 IM 如果有多个服务，仍然可以用 monorepo：",[110,444,447],{"className":445,"code":446,"language":115,"meta":116},[113],"apps\n├── im-api\n├── im-worker\n├── open-platform-api\n└── search-worker\n\ndomains\n├── identity\n├── organization\n├── group\n├── messaging\n└── notification\n\npkg\n├── observability\n├── config\n└── errors\n",[70,448,446],{"__ignoreMap":116},[17,450,451],{},"monorepo 的优点：",[123,453,454,457,460,463],{},[126,455,456],{},"跨服务重构更容易。",[126,458,459],{},"统一工具链和代码规范。",[126,461,462],{},"公共包版本一致。",[126,464,465],{},"领域模型演进时影响面更容易发现。",[17,467,468],{},"monorepo 的风险：",[123,470,471,474,477,480],{},[126,472,473],{},"容易滥用公共包。",[126,475,476],{},"边界不清时，所有服务互相 import。",[126,478,479],{},"构建和 CI 需要按变更范围优化。",[126,481,482],{},"权限和发布流程要设计好。",[17,484,485,486,77],{},"如果团队采用 monorepo，必须明确依赖规则。公共包只能放真正通用的技术能力，不能把业务模型随便放进 ",[70,487,488],{},"pkg\u002Fcommon",[17,490,491,494],{},[70,492,493],{},"common"," 往往是边界崩坏的开始。",[49,496,498],{"id":497},"_8-部署拆分的判断信号","8. 部署拆分的判断信号",[17,500,501],{},"什么时候应该把一个模块拆成独立服务？",[17,503,504],{},"可以看这些信号：",[123,506,507,510,513,516,519,522,525,528],{},[126,508,509],{},"它的发布频率明显不同。",[126,511,512],{},"它的流量特征明显不同。",[126,514,515],{},"它需要独立扩容。",[126,517,518],{},"它故障时不应该拖垮其他模块。",[126,520,521],{},"它的数据所有权已经清晰。",[126,523,524],{},"它和其他模块的交互已经稳定。",[126,526,527],{},"它有明确负责人或团队。",[126,529,530],{},"它的运维复杂度能被团队承受。",[17,532,533],{},"比如搜索上下文通常适合较早拆分，因为它的索引构建、查询负载、存储引擎和延迟容忍度都和消息主链路不同。",[17,535,536],{},"开放平台也适合独立，因为它面对外部开发者，需要配额、签名、回调、审计、事件版本管理，安全边界和内部业务不同。",[17,538,539],{},"但组织上下文未必一开始就要拆服务。如果团队还在频繁调整组织、成员、角色模型，先保持模块化单体可能更合适。",[49,541,543],{"id":542},"_9-推荐的演进路线","9. 推荐的演进路线",[17,545,546],{},"对于一个中大型协同办公 IM，我会推荐这条路线：",[548,549,550,556,562,568,574,580],"ol",{},[126,551,552,555],{},[59,553,554],{},"先做模块化单体。"," 按 Identity、Organization、Relationship、Group、Messaging、Notification、Open Platform、Search、Governance 建立代码边界。",[126,557,558,561],{},[59,559,560],{},"建立数据所有权。"," 每个上下文明确拥有的表和聚合，禁止其他上下文直接写。",[126,563,564,567],{},[59,565,566],{},"建立领域事件。"," 先用进程内事件或 outbox 表跑通事件模型。",[126,569,570,573],{},[59,571,572],{},"拆异步支撑能力。"," 优先拆 Notification、Search、Open Platform Worker、Audit Worker。",[126,575,576,579],{},[59,577,578],{},"拆高频核心链路。"," 在边界稳定后拆 Messaging。",[126,581,582,585],{},[59,583,584],{},"按团队和流量继续拆。"," 不按领域图机械拆服务。",[17,587,588],{},"这条路线比较朴素，但它尊重一个现实：业务模型稳定之前，服务拆得越细，返工成本越高。",[49,590,592],{"id":591},"_10-领域模型最终要落到工程纪律","10. 领域模型最终要落到工程纪律",[17,594,595],{},"领域边界能否落地，不取决于架构图，而取决于工程纪律：",[123,597,598,601,604,607,610,613,616],{},[126,599,600],{},"包依赖是否单向。",[126,602,603],{},"模块能否隐藏内部实现。",[126,605,606],{},"数据库表是否有明确所有者。",[126,608,609],{},"跨上下文是否通过接口或事件。",[126,611,612],{},"CI 是否能发现非法依赖。",[126,614,615],{},"代码评审是否坚持边界。",[126,617,618],{},"故障和发布是否按上下文复盘。",[17,620,621],{},"如果这些纪律没有建立，微服务也救不了系统。它只会把原来的单体泥球变成分布式泥球。",[17,623,624],{},"下一篇我们落到 Go 项目，看看在单模块和 monorepo 下，DDD 的代码、文件、包应该怎么组织。",[17,626,627],{},[21,628,630],{"href":629},"\u002Fblog\u002F","返回博客列表",{"title":116,"searchDepth":632,"depth":632,"links":633},2,[634,635,636,637,638,639,640,641,642,643],{"id":51,"depth":632,"text":52},{"id":104,"depth":632,"text":105},{"id":169,"depth":632,"text":170},{"id":245,"depth":632,"text":246},{"id":342,"depth":632,"text":343},{"id":401,"depth":632,"text":402},{"id":435,"depth":632,"text":436},{"id":497,"depth":632,"text":498},{"id":542,"depth":632,"text":543},{"id":591,"depth":632,"text":592},"架构设计","2026-06-28","md",{},true,"\u002Fblog\u002Fddd-deployment-modules",{"title":5,"description":116},"DDD 实战",7,"blog\u002Fddd-deployment-modules",[655,656,657,658],"DDD","微服务","模块化单体","Monorepo","Tdh55Lvwt3Hxn8Re0uwVDO00FHb-VMRvg4bp_GP30hQ",1783309725604]