服务边界与业务编排
服务边界的目的不是制造更多层,而是让每一段规则有稳定归属。一个服务应围绕业务能力组织,例如“创建订单”“审核内容”,而不是围绕“控制器需要什么”临时堆方法。
区分编排与规则
编排负责安排步骤:读取数据、调用规则、开启事务、触发后续任务。规则负责判断什么业务状态允许发生。两者分开后,规则能在更多入口复用,也更容易测试。
js
async function submitOrder(command, actor) {
const cart = await cartRepository.get(command.cartId)
orderPolicy.assertSubmittable(cart, actor)
return transaction(async () => {
const order = await orderRepository.create(fromCart(cart))
await outbox.enqueue({ type: 'order.submitted', orderId: order.id })
return order
})
}示例中的服务编排流程,策略对象表达业务约束,仓储对象处理持久化。命名可以变化,职责不应混淆。
事务边界围绕一个业务承诺
事务要保证“要么全部成功,要么都不发生”的那部分本地数据一致。不要把网络调用、文件上传等慢且不可控的动作长时间放在数据库事务中。
| 事务内适合做 | 事务外适合做 |
|---|---|
| 写入相关表、校验版本、记录领域事件 | 发送通知、调用外部支付、生成文件 |
| 维护同一业务承诺的数据一致性 | 可异步重试的派生动作 |
跨系统一致性通常需要幂等、事件记录、补偿或人工处理,而不是试图用一个跨网络的大事务掩盖失败。
识别过大的服务
出现这些信号时,应重新审视边界:
- 一个类同时管理用户、订单、库存和通知的规则。
- 方法参数包含十几个彼此无关的字段。
- 每次修改都牵动大量不相关测试。
- 业务术语在不同模块里含义不一致。
拆分应以共同变化的业务概念为依据。不要按数据库表机械拆服务,也不要因为“微服务”这个词就把一次本地调用变成难以排查的网络调用。
设计时多问一句
这个动作由谁发起?它改变了哪个业务状态?失败后谁需要知道?答案越清楚,服务接口就越不容易失控。