一个看似简单的功能,如果突然需要三个团队开会、修改四个服务并协调同一天发布,问题往往不在需求本身,而在系统已经失去了变更局部性:一项业务变化无法被限制在拥有该业务能力的边界内。边界漂移通常悄无声息,却会持续增加认知负担,最终让“演进式架构”只剩下口号。
从一次小改动看见边界漂移
假设电商系统要增加一条规则:VIP 客户可以免除订单取消手续费。理想情况下,这条规则应由订单或取消策略模块完成;但在边界已经漂移的系统中,团队可能发现:
- 客户等级由会员服务维护,却被多个服务复制和解释;
- 手续费计算藏在支付服务的通用退款流程中;
- API 网关根据客户类型改写请求;
- 前端通过条件分支决定是否显示手续费;
- 数据团队还依赖某个历史字段判断收入。
于是,一条业务规则变成了跨团队项目。真正的风险不只是开发周期变长,而是没有任何团队能独立回答“取消手续费到底由谁决定”。
这正是边界漂移的典型表现:实现机制逐渐扩散,业务策略却没有明确归属。每次局部优化都可能合理,但叠加之后,变更需要理解越来越多的服务、团队和发布流程。
可以用几个问题检查变更局部性:
- 一项业务规则通常需要修改多少个部署单元?
- 一个团队能否在不等待其他团队的情况下完成并发布变化?
- 同一条策略是否在前端、网关和后端被重复判断?
- 出现例外场景时,开发者是否需要绕过已有边界?
- 服务之间传递的是业务事实,还是迫使接收方重新推断业务含义的底层数据?
这些指标不要求所有变化都只改一个仓库。跨边界变化无法完全消除,但它们应该与真实业务耦合对应,而不是由偶然的代码组织方式制造。
三种恢复局部性的动作
1. 重新分配实现机制
边界漂移经常源于“复用”。团队把退款、通知或权限校验放进一个共享服务,随后共享服务开始替多个领域决定业务行为。此时需要区分两类内容:
- 机制:发送 HTTP 请求、写入消息、执行重试、保存审计记录;
- 策略:谁可以退款、手续费是多少、什么情况需要审批。
机制可以复用,策略应留在拥有业务语义的领域中。共享组件可以提供退款执行能力,但不应根据 VIP 等级自行决定是否收费。
2. 暴露必要策略,而不是泄漏内部模型
服务边界不能只提供 CRUD 数据。如果调用方必须读取十几个字段,再自行推导“是否免手续费”,策略事实上已经泄漏到了调用方。
更稳妥的接口会直接暴露领域判断,例如:
POST /cancellation-decisions HTTP/1.1
Host: orders.internal
Content-Type: application/json
{
"order_id": "ord-2048",
"customer_tier": "vip",
"reason": "customer_request"
}
响应可以明确表达决策及其依据:
{
"allowed": true,
"fee": {
"amount": 0,
"currency": "CNY"
},
"policy_code": "VIP_FEE_WAIVER"
}
这里的重点不是把所有规则塞进一个中央策略服务,而是让负责取消业务的边界拥有并表达决策。调用方获得的是稳定的业务结果,不需要复制内部条件。
3. 提前演练例外路径
正常流程通常不会暴露错误边界,例外场景才会。人工退款、部分取消、合作伙伴补偿、监管冻结和服务降级,都会迫使系统回答“谁有权改变规则”。
团队可以在设计评审或演练中提出具体情景:
- 会员服务不可用时,取消订单是否必须失败?
- VIP 等级在退款处理中发生变化,应使用当前值还是下单时快照?
- 财务人员能否覆盖手续费?覆盖决定由谁审计?
- 支付已经成功但订单状态更新失败,由哪个边界负责恢复?
演练的目标不是预测所有故障,而是找出团队在压力下最可能添加的跨边界捷径。例外路径如果没有正式入口,临时脚本、数据库直改和隐藏开关就会成为事实上的架构。
一个可运行的策略边界示例
下面是一个最小 Python 示例。它并非来源中的特定实现,而是一种可以这样实践的方式:把取消策略放在订单领域中,把支付退款保留为执行机制。将代码保存为 cancellation_policy.py,使用 Python 3.11 或更高版本运行。
from dataclasses import dataclass
from decimal import Decimal
from enum import Enum
class CustomerTier(str, Enum):
STANDARD = "standard"
VIP = "vip"
@dataclass(frozen=True)
class CancellationRequest:
order_id: str
customer_tier: CustomerTier
already_shipped: bool
@dataclass(frozen=True)
class CancellationDecision:
allowed: bool
fee: Decimal
policy_code: str
def decide_cancellation(req: CancellationRequest) -> CancellationDecision:
if req.already_shipped:
return CancellationDecision(False, Decimal("0.00"), "ALREADY_SHIPPED")
if req.customer_tier is CustomerTier.VIP:
return CancellationDecision(True, Decimal("0.00"), "VIP_FEE_WAIVER")
return CancellationDecision(True, Decimal("10.00"), "STANDARD_CANCELLATION_FEE")
def execute_refund(order_id: str, fee: Decimal) -> None:
# 这里代表支付基础设施,只执行结果,不重新解释 VIP 策略。
print(f"refund requested: order={order_id}, retained_fee={fee}")
if __name__ == "__main__":
request = CancellationRequest(
order_id="ord-2048",
customer_tier=CustomerTier.VIP,
already_shipped=False,
)
decision = decide_cancellation(request)
print(decision)
if decision.allowed:
execute_refund(request.order_id, decision.fee)
运行命令:
python cancellation_policy.py
这个结构带来三个直接效果:策略可以独立测试,支付机制不必理解会员规则,新增客户等级时通常只需要修改订单领域。实际系统还应补充幂等键、授权、审计、金额精度约束和失败恢复。
还可以用测试固定策略边界,避免规则再次散落:
from decimal import Decimal
from cancellation_policy import (
CancellationRequest,
CustomerTier,
decide_cancellation,
)
def test_vip_cancellation_has_no_fee():
decision = decide_cancellation(
CancellationRequest("ord-2048", CustomerTier.VIP, False)
)
assert decision.allowed is True
assert decision.fee == Decimal("0.00")
assert decision.policy_code == "VIP_FEE_WAIVER"
使用 pytest 运行:
python -m pip install pytest
pytest -q
不要把“局部”误解为“完全隔离”
恢复变更局部性并不意味着消灭所有共享代码,也不意味着每个团队都拥有一套重复基础设施。过度拆分会增加网络调用、数据一致性处理和运维成本;把所有策略集中到统一规则引擎,又可能形成新的协调瓶颈。
更实用的采用方式是从真实变更记录出发:选择最近几次需要跨团队协调的小需求,画出涉及的决策、数据和执行机制,然后判断哪些依赖来自真实业务,哪些只是历史代码位置造成的。优先处理高频变化路径,而不是一次性重画整套架构。
上线前可以使用这份检查清单:
- 业务策略有明确的所属团队和代码位置;
- 共享基础设施执行机制,但不偷偷决定领域规则;
- API 返回业务含义,而不是要求调用方重复推导;
- 关键例外路径有正式接口、授权与审计;
- 架构测试或策略测试能够发现边界再次漂移;
- 团队可以独立开发、验证并发布大多数领域内变化。
演进式架构的关键不是让系统永远保持某种图形,而是让变化发生时,影响范围仍然清晰、有限且由正确的团队掌控。变更局部性一旦恢复,架构才真正具备持续演进的能力。