4. BFF与下游服务的关系:聚合、编排与代理的职责边界
在微服务架构中,BFF(Backend For Frontend)位于网关与下游业务服务之间。明确BFF与下游服务的职责边界,是避免业务逻辑下沉、保持架构清晰的核心。BFF承担三种核心角色:聚合层、编排层和代理层。每一层都有明确的职责范围,不可越界。
4.1 三层职责定义与对比
| 角色 | 核心职责 | 典型操作 | 是否包含业务逻辑 | 与下游服务的关系 |
|---|---|---|---|---|
| 代理层 | 透传请求与响应,做格式转换、协议适配 | HTTP→gRPC、字段重命名、请求头透传 | 否 | 1:1 映射,无业务理解 |
| 聚合层 | 合并多个下游服务的响应,减少客户端请求次数 | 并行调用用户服务+订单服务,合并结果 | 仅数据合并,无决策逻辑 | 1:N 调用,无状态组合 |
| 编排层 | 按业务流程协调多个服务调用,包含条件分支、顺序控制 | 先调用库存服务,再调用支付服务,最后通知物流 | 包含流程控制逻辑 | 1:N 调用,有状态协调 |
关键原则:BFF 可以包含流程控制逻辑(编排层),但绝不能包含领域业务逻辑(如价格计算、库存扣减规则、用户等级判定)。领域逻辑必须下沉到对应的下游业务服务中。
4.2 职责边界示意图
下图展示了BFF三层角色与下游服务之间的调用关系,以及业务逻辑的正确归属位置。
4.3 避免业务逻辑下沉的实战原则
业务逻辑下沉是BFF架构中最常见的反模式。以下三条原则帮助团队守住边界:
- 原则一:BFF 不持有领域状态。BFF 实例不应缓存或维护任何业务状态(如用户余额、商品库存)。所有状态查询必须通过下游服务接口获取。
- 原则二:BFF 不做业务决策。例如“用户是否满足VIP条件”、“订单金额是否达到免运费门槛”等判定逻辑,必须由下游服务完成,BFF 只负责透传判定结果。
- 原则三:BFF 不修改业务数据。BFF 只能对响应数据做展示层适配(如字段重命名、格式转换、字段裁剪),不能修改业务含义或计算衍生值。
常见陷阱:团队在BFF中实现“订单总价计算”或“优惠券叠加逻辑”,认为这只是“展示层计算”。实际上,价格计算是核心领域逻辑,必须由订单服务或价格服务负责。BFF 一旦引入此类逻辑,将导致领域模型分散、测试困难、下游服务难以独立演进。
4.4 三层职责的Go代码示例
以下代码展示BFF三层在Go中的典型实现模式,注意每层只做自己职责范围内的事。
代理层:透传请求
// 代理层:仅做协议转换,不处理业务
func (h *UserProxyHandler) GetUserProfile(c *gin.Context) {
userID := c.Param("id")
// 调用下游 gRPC 服务
resp, err := h.userClient.GetUser(ctx, &pb.UserRequest{Id: userID})
if err != nil {
c.JSON(500, gin.H{"error": "service unavailable"})
return
}
// 仅做字段重命名(展示层适配)
c.JSON(200, gin.H{
"uid": resp.Id,
"nickname": resp.Name,
"avatar": resp.AvatarUrl,
})
}
聚合层:并行合并
// 聚合层:并行调用多个服务,合并结果
func (h *OrderAggHandler) GetOrderDetail(c *gin.Context) {
orderID := c.Param("id")
// 使用 errgroup 并行调用
var orderResp *pb.OrderResponse
var userResp *pb.UserResponse
g, ctx := errgroup.WithContext(c.Request.Context())
g.Go(func() error {
var err error
orderResp, err = h.orderClient.GetOrder(ctx, &pb.OrderRequest{Id: orderID})
return err
})
g.Go(func() error {
var err error
userResp, err = h.userClient.GetUser(ctx, &pb.UserRequest{Id: orderResp.UserId})
return err
})
if err := g.Wait(); err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
// 仅合并数据,不做业务计算
c.JSON(200, gin.H{
"order": orderResp,
"user": userResp,
})
}
编排层:流程控制
// 编排层:控制业务流程,但不包含领域逻辑
func (h *CheckoutOrchHandler) PlaceOrder(c *gin.Context) {
var req CheckoutRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "invalid request"})
return
}
// 步骤1:调用库存服务预占库存(下游做库存扣减逻辑)
stockOK, err := h.inventoryClient.Reserve(ctx, req.Items)
if err != nil || !stockOK {
c.JSON(409, gin.H{"error": "insufficient stock"})
return
}
// 步骤2:调用订单服务创建订单(下游做价格计算)
order, err := h.orderClient.CreateOrder(ctx, &pb.CreateOrderRequest{
UserId: req.UserId,
Items: req.Items,
})
if err != nil {
// 回滚库存(编排层只负责协调回滚,不实现回滚逻辑)
h.inventoryClient.Release(ctx, req.Items)
c.JSON(500, gin.H{"error": "order creation failed"})
return
}
// 步骤3:调用支付服务发起支付(下游做支付校验)
payResp, err := h.paymentClient.Charge(ctx, &pb.ChargeRequest{
OrderId: order.Id,
Amount: order.TotalAmount, // 金额来自下游,BFF不计算
})
if err != nil {
c.JSON(500, gin.H{"error": "payment failed"})
return
}
c.JSON(200, gin.H{
"order_id": order.Id,
"status": payResp.Status,
})
}
4.5 职责边界检查清单
在代码评审或架构设计时,使用以下清单快速判断BFF是否越界:
| 检查项 | 是否允许在BFF中实现 | 正确归属 |
|---|---|---|
| 字段重命名、格式转换 | ✅ 允许(代理层) | BFF |
| 并行调用多个服务合并结果 | ✅ 允许(聚合层) | BFF |
| 按条件选择调用哪个下游服务 | ✅ 允许(编排层) | BFF |
| 计算订单总价、折扣金额 | ❌ 禁止 | 订单服务 / 价格服务 |
| 判断用户是否满足活动条件 | ❌ 禁止 | 用户服务 / 活动服务 |
| 扣减库存、锁定库存 | ❌ 禁止 | 库存服务 |
| 生成唯一订单号、流水号 | ❌ 禁止 | 订单服务 / ID生成服务 |
架构自检:如果BFF中的某个函数需要了解“业务规则”才能正确执行,那么这个函数很可能放错了位置。正确的做法是:BFF只问下游“结果是什么”,不问“结果怎么算出来的”。