system column十三Tech
← 返回业财专栏
ERP

业财通识30:DDD 战略设计——用限界上下文划分业务边界

2026/4/517 min read
ERPDDD领域驱动设计限界上下文架构设计十三Tech

大家好,我是十三。

从本篇开始,我们进入架构思维篇。

前面 29 篇文章,我们用业务视角理解了采购、销售、库存、财务、对账的完整流转,也用产品视角拆解了模块划分、状态机、权限和审批流的设计思路。现在,我们换一个视角:如果你是一位架构师,面对这套覆盖了企业核心命脉的庞大系统,你会怎么切分它。

我的结论是:ERP 之所以复杂,不是因为代码多,而是因为业务概念在不同场景下有不同的含义。 同一个"订单",采购部、销售部、仓储部、财务部各自理解都不一样。如果架构师不给这些概念划定边界,系统很快就会变成一锅谁都改不动的粥。

今天这篇文章,我会用 DDD 的战略设计思想,帮你理解架构师如何给复杂业务划边界。

DDD 解决什么问题?

业务定义

DDD(Domain-Driven Design,领域驱动设计) 是一种应对复杂业务系统的软件设计方法。它的核心思想不是从数据库表或技术框架出发,而是从业务领域本身出发,让代码结构跟业务结构对齐。

DDD 把设计分成两个层面:战略设计战术设计。战略设计回答"系统该切成几块",战术设计回答"每一块内部该怎么写代码"。本文只聚焦战略设计。

为什么 ERP 特别适合 DDD

业财系统是典型的复杂业务系统。它的复杂性不是来自技术,而是来自业务规则本身的交错和重叠。

举个例子。在前面第 1 到第 4 篇文章里,我们走过完整的 P2P(采购到付款)流程。采购部说的"订单"是采购订单(PO),关注的是供应商是谁、单价多少、交货期哪天。

在第 8 到第 11 篇文章里,我们走过 O2C(订单到收款)流程。销售部说的"订单"是销售订单(SO),关注的是客户是谁、售价多少、信用额度够不够。

到了第 15 到第 18 篇的库存管理环节,"订单"又变成了出库指令,关注的是从哪个库位、出哪个批次、数量对不对。

三个部门都说"订单",但内涵完全不同。如果没有清晰的边界划分,一个需求改动可能牵一发而动全身。

通俗类比

DDD 战略设计,就像给公司划分部门。

公司小的时候,所有人坐一个大平层,喊一嗓子就能对齐。公司大了以后,销售部、采购部、财务部、仓储部各自有自己的术语、流程和考核指标。部门之间通过规范接口协作,而不是所有人都挤在同一个房间里互相干扰。

限界上下文,就是软件世界里的"部门墙"。它不是阻碍协作,而是让协作有章可循。

限界上下文:给业务概念划定边界

业务定义

限界上下文(Bounded Context) 是 DDD 战略设计的核心概念。它指的是一个明确的业务边界,在这个边界内部,有一套统一的业务语言和一致的概念模型。

同一个词,在不同的限界上下文里可以有不同的含义,这是被允许的,也是必要的。

关键要理解:限界上下文不是技术模块,而是业务边界。 一个限界上下文可以对应一个微服务,也可以对应一个代码包,甚至可以只对应一个领域层。它的本质是"业务一致性的范围"。

ERP 的四大限界上下文

用我们前面 29 篇文章讲过的业务来举例,一套典型的 ERP 系统至少可以划分出以下四个核心限界上下文。

采购上下文(Procurement Context)

内部统一语言包括:采购申请、采购订单(PO)、供应商、询价、比价、三单匹配。在这个上下文里,"订单"专指采购订单,关注的核心是"以什么价格、从谁那里、买到什么"。

在前面第 1 篇到第 4 篇文章里,我们完整走过了从采购申请到发票校验再到付款的 P2P 全流程。这个过程里的所有概念,都在采购上下文里有统一的定义。

销售上下文(Sales Context)

内部统一语言包括:销售机会、销售订单(SO)、客户、报价单、信用额度、渠道。在这个上下文里,"订单"专指销售订单,关注的核心是"卖给谁、卖多少钱、能不能赊账"。

在第 8 篇到第 11 篇里,从销售机会到回款的 O2C 流程,就是销售上下文的核心业务流程。第 12 篇讨论的 CRM 客户主数据,则是销售上下文与财务上下文之间的重要桥梁。

库存上下文(Inventory Context)

内部统一语言包括:入库、出库、盘点、调拨、可用库存、安全库存、成本计价。在这个上下文里,"订单"指的是出入库指令,关注的核心是"货在哪、有多少、能不能动"。

第 15 篇到第 21 篇讨论过的入库、出库、盘点、调拨、可用库存、安全库存和成本计价,都属于库存上下文的范畴。在第 19 篇里,我们专门讨论过"为什么账上有货却不能卖",这正是库存上下文内部的业务规则在起作用。

财务上下文(Finance Context)

内部统一语言包括:会计科目、凭证、总账、应收账款、应付账款、暂估入账、冲销。在这个上下文里没有"订单"这个概念,取而代之的是"凭证"和"分录",关注的核心是"钱怎么记、账怎么平"。

我们在第 7 篇详细讲过的暂估入账,就是财务上下文里的典型业务。货到了但发票没到,财务需要用自己的语言(暂估、冲销、正式入账)来处理这个业务事件。

这种"领域复杂性"的体现,恰恰是财务上下文必须独立存在的原因。

为什么需要划分这四个上下文

你可能会问:为什么不能把所有东西放在一个系统里,用同一张订单表?

答案是:因为不同部门对"同一件事"的关注点完全不同。

采购员关心供应商交期和单价,销售员关心客户信用和折扣,库管员关心库位和批次,财务关心的是会计科目和借贷平衡。如果硬塞进同一个模型,这张表会越来越宽,每个人都要在一大堆无关字段里找自己关心的那几个。

限界上下文的作用,就是让每个"部门"用自己的语言做事,只在必要的时候通过明确的接口和其他"部门"协作。

上下文映射:它们之间如何协作

限界上下文划分完了,下一步要回答的问题是:它们之间怎么打交道?

DDD 给出了几种典型的上下文关系模式,叫做 上下文映射(Context Mapping)

五种关系类型

关系类型 通俗理解 ERP 场景示例
合作关系 双方密切协作,共同演进 销售上下文与库存上下文,销售订单需要实时查可用库存
客户-供应商 一方提供接口,另一方消费 库存上下文向财务上下文提供入库数据,财务据此生成凭证
防腐层 消费方做一层转换,保护自己不被对方模型污染 财务上下文消费采购数据时,把采购订单转成本地理解的凭证模型
共享内核 多个上下文共享一部分公共模型 商品基础信息(SKU、名称、规格)被采购、销售、库存共同使用
开放主机服务 一方以标准化接口对外开放 主数据服务(如 CRM 客户信息)通过 API 向所有上下文提供数据

ERP 上下文映射图

graph LR
    subgraph 采购上下文
        PO[采购订单]
        SUP[供应商]
    end
    subgraph 销售上下文
        SO[销售订单]
        CUS[客户]
    end
    subgraph 库存上下文
        INV[库存收发]
        LOC[库位]
    end
    subgraph 财务上下文
        GL[总账]
        ACC[应收应付]
    end
    subgraph 共享内核
        SKU[商品主数据]
    end

    PO -->|入库通知| INV
    SO -->|出库指令| INV
    INV -->|库存变动| ACC
    ACC -->|凭证| GL
    SKU -.->|共享| PO
    SKU -.->|共享| SO
    SKU -.->|共享| INV
    CUS -->|信用查询| SO

这张图展示了 ERP 各模块之间的典型协作关系。

采购上下文和销售上下文各自向库存上下文下发出入库指令,库存上下文把库存变动事件同步给财务上下文生成凭证,商品主数据作为共享内核被多个上下文共用。

注意这里的箭头方向:它不是简单的数据流向,而是依赖关系。谁调用谁、谁依赖谁,决定了系统的耦合方式和演进自由度。比如采购上下文改了供应商模型,如果库存上下文没有直接依赖它,就不会被波及。

子域分类:哪里该重兵投入

限界上下文回答"怎么切",子域分类回答"切完后怎么投资源"。

DDD 把业务领域分成三类:核心域、支撑域、通用域。这个分类直接决定了团队的技术投入策略。

业务定义

  • 核心域(Core Domain):企业的核心竞争力所在,业务逻辑独特且复杂,是必须投入最优资源自研的领域。
  • 支撑域(Supporting Domain):不是核心竞争力,但为核心域服务,业务有一定特殊性,通常自研或重度定制。
  • 通用域(Generic Domain):业务通用,无特殊性,市场上已有成熟方案,应优先考虑采购或集成。

ERP 子域分类决策表

子域 分类 投入策略 ERP 示例
订单管理 核心域 重兵自研,深度定制 销售订单、采购订单的生命周期与规则引擎
库存管理 核心域 重兵自研,深度定制 可用库存计算、安全库存策略、成本计价
财务管理 核心域 重兵自研,深度定制 总账、应收应付、暂估入账、凭证生成
审批流 支撑域 中等投入,可配置 不同单据的审批节点、审批人、流转条件
权限管理 支撑域 中等投入,可配置 RBAC 模型、数据权限、操作权限
报表分析 支撑域 中等投入,可扩展 经营看板、多维查询、BI 对接
消息通知 通用域 轻量投入,优先采购 短信、邮件、站内信,可集成第三方
操作日志 通用域 轻量投入,优先采购 用户行为记录、审计追踪
文件存储 通用域 轻量投入,优先采购 发票影像、合同附件,可用云存储服务

分类背后的判断逻辑

判断一个子域是不是核心域,关键看两个问题。

第一,这个领域是否直接决定企业的竞争优势。 对于零售企业,订单和库存就是命脉,必须自己掌控。消息通知做得再好,也不会让用户多下一单。

第二,这个领域的业务规则是否具有独特性。 不同企业的财务核算规则可能差异很大,这是无法直接买现成的。但操作日志的格式和存储方式,各家企业都差不多,没有必要自己造轮子。

这个分类的本质是资源分配的优先级。核心域投入 80%的架构精力,通用域能买就买,不要把团队战斗力浪费在重复造轮子上。

统一语言:为什么同一个词在不同上下文里含义不同

业务定义

统一语言(Ubiquitous Language) 是 DDD 的另一个核心概念。它指的是在一个限界上下文内部,业务人员、产品经理、开发工程师使用同一套语言来描述业务,并且这套语言要直接反映在代码里。

但这里有一个常见的误解:统一语言不等于"全系统统一词汇表"。统一语言的边界是限界上下文,不是整个系统。

"订单"在四个上下文中的含义差异

上下文 "订单"指什么 关注的核心属性 对应代码概念
采购上下文 采购订单(PO) 供应商、单价、交货期、采购数量 PurchaseOrder
销售上下文 销售订单(SO) 客户、售价、信用额度、付款条件 SalesOrder
库存上下文 出入库指令 库位、批次、操作类型、数量 StockMovement
财务上下文 无"订单"概念 会计科目、借贷方向、金额 JournalEntry

如果在全系统里强制使用同一个 Order 类来表示以上所有概念,代码很快就会失控。采购订单的"供应商"字段对销售毫无意义,销售订单的"信用额度"对库存也不相干。

正确的做法是:在每个限界上下文里,用各自的语言定义各自的模型。 采购上下文里有 PurchaseOrder,销售上下文里有 SalesOrder,库存上下文里有 StockMovement。它们之间不直接共享对象,而是通过防腐层进行转换。

防腐层的伪代码示例

假设财务上下文需要处理销售订单的收款事件,但它不应该直接理解 SalesOrder 的结构。这时候就需要一层转换:

// 销售上下文发布的事件
class SalesOrderPaidEvent {
  salesOrderId; customerId; amount; paidAt;
}

// 财务上下文的防腐层:把销售事件转成本地理解的模型
class FinanceAntiCorruptionLayer {
  translateToReceivable(event) {
    return {
      documentType: "应收账款",
      sourceId: event.salesOrderId,
      subjectCode: "1122",
      customerCode: this.mapCustomer(event.customerId),
      debitAmount: event.amount,
      creditSubject: "6001",
      occurredAt: event.paidAt
    };
  }
  mapCustomer(salesCustomerId) {
    return customerCodeMap.get(salesCustomerId);
  }
}

// 财务上下文消费事件
function onSalesOrderPaid(event) {
  const acl = new FinanceAntiCorruptionLayer();
  const receivable = acl.translateToReceivable(event);
  generalLedger.createEntry(receivable);
}

这段代码展示了防腐层的核心作用:外部上下文的事件进来后,先翻译成自己理解的语言,再处理。 这样,财务上下文的代码不会被销售上下文的模型污染,两边可以独立演进。

这就像公司里的翻译岗位。销售部和外资供应商开会,需要翻译把双方的语言转换成彼此能理解的表达。防腐层就是这个翻译。

总结与下一篇预告

今天我们用 DDD 的战略设计视角,重新理解了 ERP 系统的切分逻辑。

第一,限界上下文给业务概念划定边界。采购、销售、库存、财务各自有自己的语言,各自对自己的模型负责。

第二,上下文映射定义了它们之间的协作方式。合作、客户-供应商、防腐层、共享内核、开放主机服务,每种关系对应不同的耦合策略。

第三,子域分类决定了资源投入优先级。核心域重兵自研,支撑域适度投入,通用域能买就买。

第四,统一语言在上下文内部消除歧义,而防腐层在上下文之间保护各自模型的独立性。

DDD 战略设计的本质,不是把系统拆得越碎越好,而是让每一块边界内的业务逻辑尽可能自洽,边界之间的交互尽可能清晰。 拆得不好,系统会变成一盘散沙;完全不拆,系统会变成一潭死水。

在下一篇文章中,我们会进入 DDD 的战术设计:限界上下文内部,聚合、实体、值对象、领域服务该怎么组织。如果说战略设计是"怎么切蛋糕",战术设计就是"怎么把每一块蛋糕做得好吃"。


往期回顾


关于十三Tech

资深服务端研发工程师、架构师、AI 编程实践者。
专注分享真实的技术实践经验,持续记录企业系统、架构设计与 AI 编程实践。