03 · 战略设计(下):限界上下文与上下文映射 一个商品三种模型我拆掉的那个 300 字段上帝类这是「DDD 实战系列」的第 3 篇。上篇02讲了子域三分法和通用语言把业务地图铺开了。本篇进入战略设计的下半场地图划完之后每块业务内部的模型边界到底在哪边界和边界之间又该怎么打交道一、先回答一个问题你团队里有没有一个 Java 类光字段就列了三四百行IDE 自动补全要翻好几屏它大概率叫Product、Goods或者Item注释里写着全公司商品的唯一真相。我见过最夸张的一个有 318 个字段。目录要它、库存要它、订单要它、营销要它、报表还要它。所有人都在往里塞字段所有人都觉得商品嘛当然就是这个类。但今天我想先给你一个反直觉的结论商品这个词在系统的不同角落根本不是同一个东西。把它们硬塞进一个类是我见过最贵、也最隐蔽的一笔技术债。这篇就讲清楚什么时候该把商品切成三个模型以及切完之后这些模型之间该怎么协作。二、先看一段我们都写过的代码下面这种上帝类我猜你要么见过要么正在维护// 全公司商品的唯一真相一个 300 字段的 Product class Product { // —— 商品目录维度 —— String title; // 标题 String subtitle; // 副标题 String description; // 详情 String coverUrl; // 封面 ListString detailImages; Category category; ListTag tags; // —— 营销维度 —— String slogan; // 营销文案 Integer discountType; // 折扣类型 String promotionCopy; // —— 库存维度 —— String sku; // SKU 编码 Integer stockQty; // 库存数量 Integer availableQty; // 可售数量 String warehouse; // 仓库位置 Integer warnThreshold; // 预警阈值 // —— 订单维度 —— BigDecimal dealPrice; // 成交价快照 String dealNameSnapshot; // 下单时名称 // …… 还有 280 个字段 }这个类能跑而且能跑很久。问题在于商品目录页只需要标题、图片、详情却被迫加载和维护全部 318 个字段库存模块只关心 SKU 和数量但改库存字段要动整个Product每次发布都得把所有消费方一起带上线最致命的是订单它本来只需要下单那一刻的成交价和名称却直接引用了活的Product。于是哪天运营改了商品名、调了价历史订单的语义被污染了——你已付款的订单突然显示成了改价后的新名字。三、 我的设计笔记 · 一我拆掉过一个 300 字段的Product上帝类代价比想象中具体。那次不是我想拆是被迫的营销团队要在大促前加一个直播专属价字段按理只动营销维度。结果因为字段挂在共享的Product上库存服务和订单服务都得跟着重新编译、回归、发版。三个团队因为一个字段的发布窗口吵了两天。更惨的是历史订单。改价功能上线当晚客服开始收到投诉用户说我买的时候是 99怎么订单里变成 199 了。根因就是订单直接引用了活的Product价格一变历史快照全跟着变。我学到的第一条上帝类不是复用是全员连坐。一个字段改不动是因为它和三百个不相关的字段绑在同一张表里、同一个类里、同一次发布里。四、限界上下文模型的户口所在地DDD 给这个病开的药方叫限界上下文Bounded Context。一句话定义限界上下文是模型的适用边界。在这条边界之内通用语言里的每一个词都有唯一、明确的含义。判断要不要切上下文也有一句话当同一个词在不同场景下含义或规则不同时就该切上下文了。商品在目录里是标题 图片 文案在库存里是SKU 数量 仓库在订单里是成交那一刻的价格与名称。它们的含义和规则都不一样——这就是三条清晰的上下文边界。反过来说如果硬把它们塞进一个类通用语言就开始一词多义代码里出现的product.getPrice()你永远不知道它返回的是目录标价、大促价还是用户实付价。这种歧义不会报错但会在某次需求变更时咬你一口。五、一个商品的三张面孔把上帝类拆开正确的姿势是三个模型各回各家商品上下文Product关心标题、详情、图片、分类、标签、营销文案、上下架状态。它是卖给用户看什么。库存上下文StockItem关心 SKU 编码、占用数量、可售数量、预警阈值、仓库位置。它是仓库里还剩多少。订单上下文OrderItem只关心成交价快照、购买数量、下单时的名称快照。它是这一单当时成交成了什么样。它们都叫商品相关模型但没有一个是同一个东西也不应该是同一个东西。三者之间靠领域事件或 API 同步数据互不侵入对方的模型三个关键认知我单独拎出来订单上下文的OrderItem绝不引用商品上下文的Product对象——只存下单那一刻的快照。否则商品改价、改图历史订单语义就被污染这正是第二节说的那场事故。上下文之间靠领域事件或 API同步数据而不是共享对象引用。切分之后每个上下文可以独立演进营销加字段库存和订单完全无感。六、上下文数量怎么把握新手常走两个极端要么只切一个大上下文回到上帝类要么切出二三十个微服务焦虑。怎么判断看信号过少的信号一个上下文里大量模型字段可空同一张表塞了多套语义同一个词在代码里有两个含义product.price既指标价又指实付改一处影响多个不相关模块。过多的信号一个上下文里只有一个类找不到第二个居民跨上下文调用占了请求耗时的大头一次下单串起八个上下文团队规模撑不起这么多上下文的独立维护。经验值一个限界上下文对应一个两披萨以内的小团队能在数周内完整理解并维护的模型。宁少勿滥——上下文越少集成的成本越低。等真的痛了再拆比提前过度设计要便宜得多。七、上下文之间怎么打交道六种映射模式切完边界上下文之间必然要协作。DDD 把这种协作关系总结成一组上下文映射Context Mapping模式。下面这张表覆盖最常用的几种请你结合自己的系统对一遍模式一句话解释典型场景协作成本合作关系Partnership两上下文互相依赖、需同步进化变更要联合排期同一迭代内紧密耦合的两个子域中联调成本高共享内核Shared Kernel两上下文共享一小部分模型需协同变更同团队维护的极紧密模块高慎用客户-供应商Customer/Supplier下游定义需求上游按需供给协商排期库存服务于订单低尊奉者Conformist下游完全顺从上游模型不做翻译对接业界标准 / 无法改造的强模型低但丧失自主权防腐层ACL两上下文间加一层翻译保护自己的模型对接三方支付、遗留系统中开放主机服务 发布语言OHSPL上游提供标准化协议与公开报文格式统一认证、领域事件 Schema一次性高、边际低另谋他路Separate Ways互不集成靠人工 / 文件同步一次性数据导出最低注意开放主机服务OHS和发布语言PL通常成对出现上游把能力暴露成一套稳定协议OHS双方约定公开的报文 / 事件格式PL所有下游按这套语言接入避免每个下游各写一套对接。最常用、也最该养成的习惯防腐层一条铁律凡是调用不受你控制的系统——三方接口、遗留系统、合作方的服务——中间必须垫一层翻译对象ACL绝不让外部报文字段直接穿透进你的领域模型。// 三方支付回调原文字段含义由别人定义随时会变 class ThirdPartyPayNotify { String tranStat; String amt; String merId; } // 防腐层翻译成自己领域的事件 class PaymentAcl { PaymentReceived translate(ThirdPartyPayNotify notify) { return new PaymentReceived( OrderId.of(notify.merId), Money.of(notify.amt), mapStatus(notify.tranStat)); // 01成功 → PAID翻译规则集中一处 } }好处极其具体三方改字段、换服务商你只改PaymentAcl一个类你的领域模型全程不认识三方的报文格式。防腐层就是一道防火墙把别人的脏数据挡在边界之外。和它对举的是尊奉者Conformist——下游完全顺从上游模型不翻译。它适用于上游模型非常强势、标准化、且你无力改造比如对接一个业界通用的标准协议。但代价是丧失模型自主权上游的一个糟糕命名你只能照单全收。什么时候选 ACL、什么时候认命做尊奉者看你能不能、值不值得为干净的内部模型付那一层翻译的维护成本。八、 我的设计笔记 · 二关于共享内核和防腐层我踩过的坑是过早共享内核。两个团队一开始关系好图省事把一部分模型抽成共享内核结果三个月后其中一个团队想重构另一个团队拦着你动了我的字段。共享内核变成了组织上的连体婴——谁都动不了因为一动就要两边一起排期。后来我的默认选择是跨团队、跨信任边界一律上防腐层只有同一个 pair、同一段代码、今天改明天就能一起验证的极紧密模块才考虑共享内核。自治带来的那点重复模型成本远低于被连体婴拖住发布窗口的代价。一句话共享内核是信任红利不是省事红利。信任还在时它是糖信任破裂时它是雷。九、一张完整的电商上下文映射图把上面这些模式拼起来一个典型电商的上下文映射长这样读这张图三个要点箭头方向 依赖方向。交易上下文是核心域它依赖的支撑域库存、物流、营销按它的节奏供给——这就是客户-供应商关系订单是客户库存是供应商。对外部系统支付网关一定经过 ACL图里能看到那条明显的隔离带。任何外部依赖都不要裸接。认证是典型 OHS一个上游服务所有下游大家都按同一套 Token 协议接入避免每个上下文各自对接一套登录逻辑。十、映射图是组织架构的镜子上下文映射还有一层更深的含义它是组织架构的镜子这就是康威定律——系统结构会复刻沟通结构。映射模式背后的团队关系客户-供应商两个团队下游有话语权排期可协商防腐层两个团队互不信任或外部无法协商各管各的共享内核同一个团队或两个 pair 极紧密的团队另谋他路没有正式协作渠道警惕多半是组织问题一个实用推论如果你在映射图上看到共享内核先别高兴先检查是不是两个团队在互相改对方的代码——如果是多半该拆团队或拆上下文而不是共享模型。反之两个团队天天因为接口吵架映射关系却还是客户-供应商那说明组织上该给下游更多话语权了。十一、两个常见疑问Q限界上下文就等于微服务吗不是但关系密切。限界上下文是逻辑边界微服务是物理部署单元。理想状态是一个上下文一个服务但落地时完全可以是多个上下文先住在一个单体里各自独立模块等业务真痛了再拆。09 篇会详细展开 40 万行单体如何按上下文渐进拆分。Q怎么验证上下文边界划对了两个体检项① 每个上下文内部能不能用 5~10 句通用语言讲清自己的职责② 跨上下文的调用或事件是不是在真实业务流程中自然出现还是为了凑数据硬拉的。后者基本说明边界划错了。最后把上下文划分的几个判断做成一张自查卡贴在这里方便你收藏十二、我为什么要写这个系列本篇小结战略设计讲到这就告一段落。两篇加在一起我们其实只回答了一件事怎么在动手写代码之前先把业务的地盘划清楚——子域解决哪些能力重要限界上下文解决每个词在哪套规则里说话算数上下文映射解决地盘之间怎么签契约。划边界这件事前期看着像多画了一张图但它换来的是后面每次需求变更时的确定性改营销不惊动库存接三方不污染内核加下游不侵入主流程。这也正好接上下一篇——边界划好之后边界内部的类到底该怎么写系列目录篇主题一句话00系列导读这条学习路线怎么走三类人怎么读01为什么需要 DDD规则副本的代价与三条原则02战略设计上子域三分法、事件风暴、通用语言表03战略设计下本文一个商品三种模型限界上下文与六种上下文映射模式04战术设计上实体、值对象、聚合的建模与三条铁律05战术设计下领域服务、领域事件、仓储与工厂06架构模式四层架构、六边形架构与依赖倒置07实战案例一电商订单系统聚合与事件驱动建模08实战案例二银行转账跨聚合的最终一致性09实战案例三40 万行单体如何按上下文渐进拆分10落地与误区六步路线图、七个高频坑、体检清单下一篇04 · 战术设计上实体、值对象与聚合—— 边界内部的类到底该怎么写setStatus(2)这种后门为什么是隐患一致性边界又该画在哪儿。关于作者技术原点YuandianIT985 计算机硕士十年一线研发经历先后在头部互联网公司做交易与履约现于金融行业负责信贷核心、风控与账务系统的架构设计累计面试近千人带过 50 新人长期负责技术团队的技术选型与架构评审关注方向复杂业务系统建模DDD / 事件驱动 / 一致性设计与AI 工程化落地写作原则只写自己动手做过的只给能落地的结论方案必须带代价我信一句话架构的优劣不在图上在第三次需求变更时的手感上。关于技术原点始于技术归于原点—— https://yuandianit.com技术原点是一个基于Spring Boot 3 Vue 3前后端分离的自有技术平台把「内容创作 付费专栏 AI 辅助 资源交易」完整跑通——不是一个博客 Demo能力说明 文章系统Markdown 创作、分类标签、归档时间线、阅读量 / 点赞 / 收藏、关键词搜索 Wiki 知识库专栏化知识库、目录树导航、整册解锁 ——本 DDD 系列会整理成册放在这里 AI 助手接入智谱 / OpenAI / 通义千问 / DeepSeek 多模型支持 SSE 流式追问 付费阅读四级阅读门禁与付费专栏支持单篇与整册 社区商城源码与服务商品、微信 Native 扫码支付、订单与下载鉴权⚙️ 后台管理内容与用户管理、sa-token 权限、操作日志、定时任务、Knife4j 接口文档为什么要到平台上读这个系列代码和图都在线渲染架构图、代码高亮、一键复制比看截图舒服读到不懂的地方直接问 AI平台内置多模型 AI 助手可以就某一篇继续追问不必等我在评论区回成册阅读系列在平台上按 Wiki 专栏组织目录树跟随适合系统学一遍平台本身就是源码文章里讲的依赖倒置、事件驱动、支付闭环、AI 流式对话都能在平台源码里找到真实落地——理论和实现互为注解。立即访问https://yuandianit.com