从战术代码冲进战略设计之前,我想先劝你冷静一下。很多团队学DDD(领域驱动设计)是这么个流程:买书、看视频、装上几个框架、照着样例写了个聚合根,然后回到自家老项目里发现根本无处下手。问题不出在聚合、仓储、应用服务这些战术概念上,而是大多数团队直接从战术设计开始了DDD。没有战略设计划定的边界,你再怎么写聚合根都是在老地图上找新大陆。这就像装修队进场之前,房子几室几厅、哪堵墙能拆、哪根柱子不能动都没定,工人再勤奋也是白忙。
这些年我带过的项目里,凡是DDD落地到一半崩掉的,几乎都卡在同一条线上:业务边界不清。代码写得再好,领域模型再漂亮,只要边界划错,后续每一次迭代都会为当初的错误买单。本文打算把战略设计这件事讲透,重点放在子域(Subdomain)和限界上下文(Bounded Context)这两个概念上,顺手把热词里的菱形对称架构也串进来,告诉你边界定完之后代码该怎么顺着边界长出来。
1. 战略设计入门课:别急着写代码,先弄清边界究竟在哪
1.1 为什么战术模板救不了混沌业务
先说个现象。网上搜DDD,冒出来最多的内容是仓储模式、聚合根、领域服务、值对象、CQRS,这些都是战术设计里的东西。它们解决的问题是"在一个已经划好的业务边界内部,如何让领域模型稳定、代码结构清晰"。但如果你还没回答"边界在哪里",这些战术手段就像给一团乱麻套上一个个漂亮的塑料壳,壳里照样是乱的。
我见过一个做供应链系统的团队,他们很认真地建了十几个聚合根,结果每个聚合根之间互相直接调用对方的仓储接口,领域对象绕成了一圈。问他们为什么这么建,答案是"按数据库表拆的"。这就是典型的没做战略设计。数据库表是从关系视角看业务数据,DDD的边界是从业务能力和业务语义的视角看世界,两者错位是必然的。
1.2 战略设计只回答两个问题:什么是核心、边界在哪
战略设计落到实操层面,其实就是在做两件事:
- 把一块大业务拆成不同价值的业务领域,也就是子域(Subdomain),回答"哪块业务是核心竞争力,哪块是辅助,哪块可以直接买现成的"。
- 在业务内部找到模型边界,也就是限界上下文(Bounded Context),回答"哪些概念在哪个业务场景里成立,哪些概念出了这个场景就该换一种说法"。
这两件事层层递进:先定价值,再定边界。很多人把子域和限界上下文混为一谈,其实它们解决的是不同维度的问题。子域是业务问题空间,限界上下文是解决方案空间。子域回答"这是什么业务",限界上下文回答"这套模型服务于谁、语言边界划到哪"。
1.3 业务建模和领域建模:两者不是一回事
这里必须把概念理清。业务建模是站在公司经营者的视角,搞清楚公司到底靠什么赚钱、哪些能力决定了市场竞争力、哪些流程只是为了支撑主流程运转。领域建模是站在软件系统内部,把业务概念模型化成代码里可以落地的对象、服务、行为规则。
战略设计恰好卡在两者中间:它既要理解业务价值,又要把价值转化为软件边界。这条桥如果走不过去,后面的战术设计就是空中楼阁。这也是为什么很多"网上学来的DDD笔记"一到真实场景就失灵——因为它们根本没讲清楚怎么走这座桥。
2. 子域划分:怎么判断一块业务到底值不值得"亲自下场"
2.1 核心域、支撑域、通用域的区分维度
子域是DDD里最接近业务的语言。它把企业的业务能力分成三类:
- 核心域(Core Domain):公司区别于竞争对手、决定市场地位的核心竞争力所在。这块业务没有现成方案可用,必须自己投入顶尖资源打磨。
- 支撑域(Supporting Domain):核心业务必不可少的辅助能力,但本身不构成差异化竞争力。比如电商里的订单履约支撑,做得好是应该的,做不好会拖累核心业务,但它不会让客户只因为你在线上开店。
- 通用域(Generic Domain):市场上已有成熟方案,买现成的比自己开发更划算。比如权限、通知、短信、支付网关这类。
判断标准有个很实用的口诀:如果这块业务做砸了,你的业务会直接受损,而且别人很难替代你的实施质量,那它就是核心域;如果做砸了影响主流程,但市面上有成熟方案可以直接买,那它是通用域;如果你的团队实在没人手,想外包给第三方做,说明它不是核心域。 这句话说出来可能会让一些产品经理不高兴,但确实越早承认核心域之外的东西可以借力,团队资源就越能集中在真正让业务起飞的地方。
2.2 事件风暴是找核心域性价比最高的方式
怎么落地方能识别核心域?我最推荐的方法是事件风暴(Event Storming)。
准备一面足够大的墙和一批便利贴,约上业务专家、产品、开发、测试一起坐下来,从业务的时间线出发,从起点写业务事件。比如订单系统,事件可能是"客户提交订单""库存锁定成功""支付完成""仓库发货""物流签收"。
贴着贴着你会发现,有些事件扎堆出现,旁边围了一堆业务规则,这些地方通常就是需要关注的核心流程。事件之间的因果链越密集,越说明这里是业务命脉。这就是一个不需要画复杂的架构图、不需要会UML,只需要懂业务的领域专家配合就能完成的手段。
2.3 核心域不等于流量最高的域
一个常见的误区是把用户量最大、页面访问最多的模块当成核心域。实际上,核心域的判断要看差异化竞争力,而不是流量。
拿打车软件举例,用户端APP可能流量巨大,但它的核心域其实是调度引擎——能高效匹配供需、动态调价、规划车辆路径。调度引擎做得差,用户再用也觉得难用,而用户端的界面流畅度,市面上随便一个成熟客户端团队都能做出来。所以核心域是调度,用户端反而更接近支撑域。
判断核心域时,问自己一个问题:隔壁竞争对手想抄你,他最想挖走的是你的哪个团队?那个团队做的东西,就是你的核心域。
3. 限界上下文:把业务语义切成一刀一刀
3.1 限界上下文切分的核心原则:不是按模块,是按语义一致性
限界上下文(Bounded Context)是DDD里翻译得有点绕但极其重要的概念。它本质上是同一个业务概念在不同语境下的翻译边界。
"用户"这个词,在营销上下文里叫"潜在客户",你关心的是他的画像、渠道来源、转化漏斗;在客服上下文里叫"工单发起人",你关心的是他的咨询记录、满意度;在订单上下文里叫"收货人",你关心的是他的地址、联系方式。如果强行用一个"用户"对象同时满足三个场景,你会得到一个巨大无比、属性极多、字段无数为空的对象。这个对象就是典型的"上帝对象",代码里的每个需求都来这里"薅属性",最后谁都不敢改它。
限界上下文就是给模型划一个"语境圈"。在圈内,每个词都有明确的、唯一的含义,这叫通用语言(Ubiquitous Language)。圈外的人听不懂圈内的话没关系,通过翻译(上下文映射)来协作即可。
3.2 五个常用的切分驱动力
怎么判断应该切成几个上下文?没有标准答案,但我总结了五个高频驱动力,只要命中任意一个,就值得考虑拆开。
- 语义冲突:同一个词在不同业务场景里含义明显冲突。比如"客户",销售语境和财务语境里的定义完全不同,销售关心意向等级,财务关心账期和信用额度。
- 变更频率不同:一部分业务每周都在迭代,另一部分几个月不动一次。把变化快和变化慢的混在一个上下文里,会导致每次发布都拖着整个包裹一起走。
- 团队边界:不同团队负责不同模块,团队之间的沟通成本高,上下文边界应该和团队边界对齐,让模型尽量在团队内部自治。
- 技术约束:某些场景对性能、安全、事务的诉求极端不同。比如实时风控和后台报表,放在一起互相拖累。
- 独立部署的需求:有些业务需要独立扩缩容、独立发版,这时候别硬塞进一个上下文。
3.3 上下文之间的映射关系与防腐层
上下文切好之后,上下文之间不是彻底隔离的,它们之间有协作,协作方式就是上下文映射(Context Mapping)。我不打算把七种映射关系全部展开,只讲最常用的三种:
- 防腐层(Anti-Corruption Layer,ACL):下游上下文防止上游上下文的概念污染自己的模型,在下游入口处做一层翻译和隔离。这是最值得优先掌握的映射关系。比如订单上下文对接物流平台,物流平台有自己的运单模型和回调数据结构,订单上下文不应该直接引用物流的模型,而应该在入口处做一个防腐层,把物流的"运单"翻译成自己上下文里的"物流记录"。
- 共享内核(Shared Kernel):两个上下文共用一块很小的模型,通常是枚举、公共值对象或少量共享代码。好处是省事,坏处是共用部分一改,两边都要跟着变。我建议共用范围越小越好,最好只有枚举和纯值对象。
- 客户-供应商(Customer-Supplier):上游是供应商,下游是客户。上游接口怎么变,下游基本没有话语权,只能适应。最常见的就是对接第三方支付、第三方物流,他们给你的接口就是上帝视角,你只能顺着做。
4. 从战略设计到菱形对称架构:边界定好之后,代码怎么长出来
4.1 菱形对称架构与限界上下文的对应关系
"DDD菱形对称架构"是这两年社区里讨论度比较高的一个实现风格。它的核心思想是:一个限界上下文对应一个代码形态自洽的结构,这个结构看起来像一个菱形。
菱形上端是应用服务层(Application Service),它接收外部指令,编排领域层的行为,不写业务规则;菱形中央是领域层,有聚合、实体、值对象、领域服务,业务规则都留在这一层;菱形左右两端是适配层,左侧是入站适配器(接收REST、RPC、MQ消息),右侧是出站适配器(访问数据库、调用外部服务);菱形下端是基础设施层,负责技术细节的具体实现。
这个形态和战略设计是天然互补的:战略设计确定了限界上下文边界,菱形对称架构就告诉你在一个边界内部,代码应该以什么姿势组织。一个上下文一个菱形,上下文之间的通信走接口适配层,通过防腐层翻译。
4.2 每个上下文内的代码结构如何组织
很多团队一上来就纠结"我该用几个项目""仓库怎么分"。我的建议是:先从物理边界和依赖方向入手,不要一开始就纠结具体框架。
一个限界上下文内部,通常可以拆成这样几个模块:
- application(应用服务,编排用例,事务边界一般在这里)
- domain(领域模型,策略模式、领域服务、聚合根都在这)
- adapter(入站适配器和出站适配器,负责协议转换)
- infrastructure(数据库访问、消息发送、外部客户端等实现细节)
依赖方向从外向内:adapter依赖application和domain,application依赖domain,infrastructure实现domain里定义的仓储接口。这样方向画出来,domain就是最纯粹的,不依赖任何技术框架。测试时domain可以做纯单元测试,不需要数据库,不需要启动Spring。
5. 案例实操:用"订单+仓储"场景走一遍完整战略设计
5.1 从业务事件清单开始
为了把抽象概念落到地面上,我拿一个常见的订单+仓储库存场景走一遍完整流程。
先组织事件风暴。召集业务方、产品和开发,大家一起贴业务事件。拿到的原始事件清单大致是这样的:
- 客户提交订单
- 订单已受理
- 库存预占成功
- 支付完成
- 库存扣减
- 订单推送至仓库
- 仓库拣货完成
- 仓库发货
- 物流签收
- 客户申请售后
- 仓储调拨
- 盘点差异调整
5.2 判断核心域并切分上下文
把这些事件按业务能力归类,可以得出四个明显的业务能力簇:
第一簇是围绕"订单"的:提交订单、受理、支付、售后。第二簇是围绕"库存"的:预占、扣减、调拨、盘点。第三簇是围绕"履约"的:推送仓库、拣货、发货、签收。第四簇是围绕"支付"与"通知"的:支付回调、发送通知。
对照子域分类标准:订单和履约是这家公司的核心域吗?这要看公司定位。如果这家公司做的是精品自营电商,那么订单与库存协同就是核心竞争力的关键,属于核心域。如果它只是接了个第三方仓储系统来跑,那么仓储调度反而是供应商的通用域。现实中很多业务没有唯一正确答案,要看公司战略,这正是战略设计有趣的地方。
我的建议是,先把该电商业务的核心域定为"订单+库存协同",支撑域是"履约对接"和"支付对接"。因为订单和库存的协同规则(比如超卖控制、拆单逻辑、库存占用策略)决定了毛利和客户体验,属于差异化竞争力。
供应商能力都成熟,直接购买或者API对接,属于通用域。通知服务用现成的消息平台,属于通用域。这样划分之后,上下文就可以定了:
- 订单上下文:负责订单生命周期、拆单、订单状态机。
- 库存上下文:负责预占、扣减、调拨、盘点。
- 履约上下文:负责对接WMS,把订单变成出库单,推进物流状态。
- 支付上下文:负责对接支付渠道,维护支付记录。
- 用户上下文:负责C端用户资料、收货地址。
- 通知上下文:负责短信、App推送。
5.3 上下文映射落地:团队和仓库怎么分
上下文切好后,再看它们之间的映射关系,这样团队划分和代码仓库划分就都顺理成章了。
订单上下文和库存上下文之间,是典型的上游-下游关系。订单需要"预占库存",如果用共享内核直接共用一张库存表,两边改起来都很痛苦。我在设计时会做一个独立的库存客户端,订单上下文通过防腐层调用库存上下文的接口,订单侧只保存"预占结果"和"预占单号",不直接操作库存表。这样两边各自的模型改动互不干扰。
订单上下文和履约上下文之间,需要做状态同步。订单状态里,客户关心的是"支付成功、已发货、已签收";履约上下文里,WMS关心的是"波次号、拣货位、包裹号"。这两棵概念树完全不在一个坐标系,强制映射会让订单模型的字段迅速膨胀。因此我在履约上下文入口做了一层防腐层,把WMS回调消息翻译成订单自己关心的状态变化。
6. 战略设计最常见的几个坑与排查方法
6.1 子域和限界上下文混为一谈
这个坑在所有新手里几乎都会踩一次。子域和限界上下文不是一回事,它们之间的关系是"业务价值和软件边界的映射"。
一个子域内部可以拆出多个限界上下文。比如"库存子域"里,"仓库现场操作"的上下文和"总部库存规划"的上下文模型差得很远。反过来,一个限界上下文也可能横跨多个子域吗?可以,但通常不建议。如果一个上下文同时承担了核心域和通用域的责任,就会出现"核心逻辑和被外包业务裹挟"的窘境。
排查方法很简单:画一张表格,左边列子域,右边列限界上下文,逐个写清楚两者的映射关系。如果发现一个限界上下文同时注释着"这是核心域"又出现"这块业务可以直接买现成工具",那就要警惕了,大概率边界已经糊了。
6.2 上下文边界与数据库边界对不上
很多团队上下文画得很漂亮,到了数据库设计时还是一个大库,所有表放在同一个数据源里,表之间自由外键关联。这样做的结果就是代码层面的上下文边界形同虚设,你虽然把模块拆了,但任何一个服务都能直接join别人核心上下文的数据表,上下文隔离瞬间失效。
正确做法是:一个上下文一套模型,数据库物理边界能拆就拆,至少也要做到表归属明确,禁止跨上下文直接查表。拆不开的高成本遗留库,也要约定好通过接口访问,不直接共享表。这个约束在代码评审阶段就要卡住,等上线之后再改,成本极高。
6.3 过度切分让协同成本暴涨
说完一个极端,再讲另一个极端。刚刚学战略设计的人容易陷入"一切都要拆"的兴奋里,把公司业务切成了几十个上下文,每个上下文只有两三个接口。结果就是一件简单的下单操作要跨越七八个服务,链路一长,出问题都不知道找谁。
切分的底线是"高内聚、低耦合",但这个原则需要量化。我自己的经验是:
- 如果一个上下文在半年内的需求变更次数连十个都不到,说明它可能拆得太细了。
- 如果一次需求迭代要联动修改超过三个上下文,这个粒度可能在业务上给团队造成了明显负担。
- 上下文之间的依赖方向尽量保持单向,避免双向依赖形成环。
一句话,边界是为了让变化可控,不是为了追求数量。一个合理的上下文粒度应该是"一个业务能力域加一个明确的业务语义圈",能独立演进,有问题能在内部闭环解决。
6.4 防君子不防小人的防腐层误用
防腐层是战略设计里含金量最高的映射手段,但也是最容易被用歪的。有团队把防腐层做成了一层纯粹的DTO转换,每调一个外部接口就写一堆出参入参的copy代码,然后告诉自己"我有防腐层"。
真正的防腐层做的不是简单的字段映射,是对抗语义污染。它要把外部上下文里不符合自己模型的概念挡在门外。订单上下文对接物流时,物流回调的"包裹轨迹节点列表"不应该带着物流方的前置仓编号、分拣机编码等内部字段直接进入订单领域模型。防腐层要把这些外部噪音过滤掉,只翻译成订单上下文关心的"已发货、运输中、已签收"这几个业务状态。
排查防腐层是否合格有个简单问题:上游模型塞给你一个新字段,你的防腐层是改一行映射加字段,还是会让领域模型本身跟着变?如果每次上游变化都渗透到你的领域模型里,说明防腐层已经失效了。
战略设计这东西,看起来不像写代码那样能立刻看到产出,但它决定了一个系统能走多远。花一两周时间把业务边界推清楚,后面战术实现会顺一大截;反过来,边界不清带来的重构成本,会在未来以月为单位偿还。我自己在多个项目里验证过,只要你愿意在动手写第一行代码之前,先静下来把子域的价值排序和限界上下文的边界画出来,DDD的优势才能真正发挥,菱形对称架构这类实现风格才有立足之地。
