BFF 架构模式

BFF(Backend for Frontend)——为特定前端体验塑形的后端适配层,解决多端 API 契约差异、聚合裁剪、与 API Gateway 的职责边界问题。

BFF 架构模式

BFF 是 Backend for Frontend 的缩写。这个名字本身就说明了它的定位:它是一层专门为前端打造的后端服务。

这个词最早由 Sam Newman 在 2015 年提出,当时他在 ThoughtWorks 观察到很多团队在微服务架构下,前端和后端的交互契约变得越来越难管理。他的核心洞察是:与其让一个通用 API 服务所有前端类型,不如为每种前端定制一个专属的后端接口层。这个观察到今天依然有效。

问题的根源:数据结构的不匹配

要理解 BFF 解决了什么问题,先要看清楚大多数项目中前端和后端的天然矛盾。

后端服务的边界是按业务领域划分的。一个典型的电商系统会有商品服务、库存服务、促销服务、用户服务、订单服务。后端工程师设计这些服务时,关注的是领域逻辑的正确性和内聚性,不会考虑某个页面需要显示什么数据。

但前端工程师设计页面时,关心的是用户看到什么。一个商品详情页需要的数据来自五六个不同的后端服务,每个服务的返回字段有几十个,但页面真正用到的可能只有十来个。前端工程师不得不发多个请求、等多个响应、处理多种错误格式,然后自己在客户端拼装数据。

举一个具体的数字。如果不做 BFF,前端要展示一个完整的商品详情页,通常需要这样的请求序列:

// 没有 BFF 时,前端需要自己组合这些数据
async function loadProductDetail(productId, userId) {
  // 这些请求相互独立,可以并发
  const [product, inventory, promotion, reviews, recommendations] = await Promise.all([
    fetch(`/api/products/${productId}`),
    fetch(`/api/inventory/${productId}?userId=${userId}`),
    fetch(`/api/promotions/active?productId=${productId}`),
    fetch(`/api/reviews/summary?productId=${productId}`),
    fetch(`/api/recommendations/${productId}`),
  ]);

  // 然后前端要做大量数据清洗和组合工作
  const productData = await product.json();
  const inventoryData = await inventory.json();
  const promotionData = await promotion.json();
  const reviewsData = await reviews.json();
  const recommendationsData = await recommendations.json();

  // 返回给组件的是一个拼凑出来的对象
  return {
    id: productData.id,
    name: productData.name,
    price: calculateDisplayPrice(productData.price, promotionData), // 前端要做价格计算
    mainImage: productData.images[0],
    isAvailable: inventoryData.stock > 0, // 前端要做库存判断
    // ... 页面真正需要的字段只是返回数据的子集
  };
}

这个过程有几个明显的问题。首先是字段暴露:前端知道了每个下游服务返回的所有字段,即使这些字段页面根本不需要,这是额外的数据泄露风险。其次是认知负担:前端工程师需要理解每个服务的返回格式,理解它们之间的关系,这本来应该是后端提供的封装。再次是变更脆弱性:任何一个下游接口改了字段,前端代码都可能受影响,即使那个字段页面根本没用过。

BFF 解决这个问题的思路是:不在前端做聚合,也不用让后端理解页面逻辑,而是在前后端之间加一层专门处理适配工作的服务。

BFF 的核心职责

BFF 这个词本身有一定的误导性,让人觉得它只是「后端再往前放一层」。实际上它做的事情比转发复杂得多。

BFF 的核心工作是把多个下游服务的响应组装成前端可以直接使用的数据结构。这个过程包括:并发请求多个下游服务、按页面需求过滤掉不需要的字段、把不同服务的字段名称和格式统一、注入用户相关的上下文信息、统一处理下游服务返回的错误。

用代码来描述这个过程会比较清晰。一个典型的商品详情页 BFF 接口大概长这样:

app.get('/api/web/product/:id', async (req, res) => {
  const { id } = req.params;
  const userId = req.user.id;

  // 并发请求下游多个服务
  const [product, inventory, promotion, reviews] = await Promise.all([
    productService.get(id),
    inventoryService.get(id, userId),
    promotionService.getActive(id),
    reviewService.getSummary(id),
  ]);

  // 按页面需求组装数据
  res.json({
    product: {
      id: product.id,
      name: product.name,
      price: product.price,
      mainImage: product.mainImage,
      description: product.description,
    },
    inventory: {
      available: inventory.available,
      quantity: inventory.stock,
    },
    promotion: promotion ? {
      label: promotion.label,
      discount: promotion.discount,
    } : null,
    review: {
      rating: reviews.average,
      count: reviews.total,
    },
  });
});

前端拿到的是一个完整的商品详情数据结构,不需要理解商品服务返回了哪些字段、库存服务返回了哪些字段,也不需要处理它们之间的关联逻辑。这是 BFF 和普通 HTTP 代理的本质区别:代理只是转发,BFF 做的是有意义的计算和组合。

BFF 还负责处理一些服务端才有的优势场景。比如用户认证信息在请求到达 BFF 时已经被解析好了,BFF 可以直接把 userId 传给下游服务,而不需要前端每次都带着用户凭证去调用各个服务。再比如某些接口在不同版本的前端里有细微差异,差异逻辑可以放在 BFF 而不是让两个版本的前端都处理这个 if-else。

架构图示

下面这张图展示了 BFF 在系统中的位置,以及数据流向。

flowchart LR
    Client["客户端<br/>Web / iOS / Android / 小程序"]
    BFF["BFF 层<br/>每个前端类型独立部署"]
    DS1["商品服务"]
    DS2["库存服务"]
    DS3["促销服务"]
    DS4["评价服务"]

    Client --> BFF
    BFF --> DS1
    BFF --> DS2
    BFF --> DS3
    BFF --> DS4

    style BFF fill:#e1f5fe
    style Client fill:#f3e5f5

可以看到,前端不需要知道下游有多少服务、每个服务的接口是什么、返回什么字段。前端只需要调用 BFF 提供的一个接口,BFF 负责请求哪些下游服务、如何组合数据。

如果产品同时服务 Web 和 iOS 两个前端,它们各自有独立的 BFF。Web BFF 和 iOS BFF 调用的是同样的下游服务,但返回给前端的数据结构可能不同——Web 端可能返回更完整的商品描述用于 SEO,iOS 端可能返回更精简的字段用于省流量。

BFF 和 API Gateway 的区别

这是 BFF 相关讨论中混淆最多的概念。两者都在前后端之间、都部署在比较靠前的位置,但职责完全不同。

API Gateway 处理的是平台层面的横切关注点:路由分配到不同的后端服务、认证令牌的验证和传递、请求限流防止系统过载、统一日志和监控、跨所有接口的通用错误处理。这些逻辑是平台基础设施的一部分,对所有前端类型一视同仁。

BFF 处理的是前端体验层面的数据关注点:某个特定页面需要哪些数据字段、不同端的数据需求差异、页面加载时的数据聚合逻辑、哪些数据需要服务端提前处理而不是让前端请求两次。这些逻辑和具体的前端实现紧密相关,不同前端类型可能有完全不同的需求。

举一个具体的边界场景。假设 Web 端需要商品详情页显示「当前用户是否可以购买」,而小程序端不需要这个字段。如果在 API Gateway 层实现这个逻辑,Gateway 就需要知道「哪些端需要哪些字段」,这实际上是在把 BFF 的职责往上移了。正确的做法是让 Web BFF 和小程序 BFF 各自实现自己的字段逻辑,API Gateway 只负责路由和认证。

有些团队会把 API Gateway 和 BFF 合在一起部署,这在小型团队里是合理的成本优化。但合在一起不等于职责混淆。 Gateway 的配置(比如限流规则、认证处理)应该只在 Gateway 层,BFF 的逻辑(比如字段裁剪、数据聚合)应该只在 BFF 层。代码放在同一个服务里不代表逻辑可以混在一起。

API Gateway BFF
归属团队 平台/基础设施团队 前端或全栈团队
关注点 平台公共逻辑 特定前端的数据需求
变更频率 低,稳定的平台逻辑 高,随前端需求变化
部署方式 统一部署,所有端共享 分前端类型独立部署
典型内容 路由规则、限流配置、认证中间件、统一错误格式 页面数据聚合、字段裁剪、用户上下文注入

什么适合放在 BFF,什么不适合

BFF 最容易出现的问题不是「不知道怎么用」,而是「用得太多」。当团队发现 BFF 改起来很快、和前端协作方便之后,很容易把越来越多的逻辑往里塞,直到 BFF 变成了一个新的业务单体。

判断一个逻辑是否应该放在 BFF,有一个简单的方法:如果把这个逻辑从 BFF 移到某个领域服务,前端调用 BFF 的方式会变吗?如果会,说明这个逻辑本身属于领域服务,不应该放在 BFF。

适合放在 BFF 的场景包括:把多个后端接口聚合成页面所需的一个数据结构、按当前用户上下文过滤返回字段、前端专属的缓存策略(页面级聚合缓存、用户个性化缓存)、简单的协议转译比如 REST 转换成内部 gRPC 调用、把前端需要但下游服务分散在多个接口里的关联数据一次性返回。

举一个具体的「这个逻辑该不该放 BFF」的判断场景。假设促销规则是:订单金额超过 100 元且用户是会员时,折扣在原价基础上再减 10%。这个折扣计算逻辑应该放在哪里?

如果放在 BFF 里:促销服务返回原始促销规则,BFF 根据用户属性和订单金额计算最终折扣。这个做法的问题在于,如果业务说「会员折扣要改成减 15%」,你需要改 BFF 代码,而且需要重新测试。如果将来还有 App 端也需要这个逻辑,你又要在 App BFF 里重复实现一遍。

如果放在下游促销服务里:促销服务接收 orderId 和 userId,返回最终计算好的折扣值。这个做法更合理,因为折扣计算是业务规则,属于领域逻辑,应该在领域服务里维护。

不适合放在 BFF 的场景包括:商品定价逻辑、库存扣减规则、促销活动的核心计算规则、跨系统事务一致性要求、需要独立扩展的能力。任何写进 BFF 之后「如果这里算错了业务就不对」的逻辑,都不应该在 BFF 里。

GraphQL、REST、tRPC:实现 BFF 的技术手段

BFF 是一种架构模式,不是具体技术。用什么方式实现 BFF 是独立的选择。

REST 聚合是最直接的做法。前端发一个 HTTP 请求,BFF 调用多个下游 REST 接口,返回聚合结果。优点是团队普遍熟悉、调试方便、HTTP 生态完善。缺点是接口文档需要额外维护、前端需要等待 BFF 接口实现才能开始联调。

GraphQL 提供了不同的思路。它的查询语言允许前端声明自己需要哪些字段,而不是由后端决定返回什么。这个特性在数据关系复杂、多个前端都需要一定灵活性的场景下很有价值。但 GraphQL 本身不等于 BFF——一个跨多个团队和客户端的共享 GraphQL 层是平台 API,而不是 BFF。只有当 GraphQL schema 按特定前端体验组织、resolver 调用的是该前端专属的聚合逻辑时,它才算是 BFF 的实现方式。GraphQL 还带来 schema governance 的成本,需要有人持续维护 schema 的一致性和向后兼容。

tRPC 是近年来兴起的一种方案,核心价值是前后端之间的端到端类型安全。它不需要代码生成步骤,后端定义的类型可以直接在前端使用,在同一仓库的 TypeScript 全栈项目中体验很好。这种模式天然适合 BFF 场景,因为 BFF 通常由前端团队或全栈团队维护,不需要对外开放接口。但它的局限性也很明显:不适合多团队共享、不适合需要对外开放 API 的场景、强烈依赖 TypeScript。

选择哪种技术,取决于团队规模、协作模式、是否需要对外开放 API。这些和技术选型本身的价值无关。

什么时候不需要 BFF

BFF 不是万能解药。有些场景下引入 BFF 只会增加复杂度,不会带来相应的价值。

如果你的系统只有一个前端类型(比如说只有一个 Web 端,没有 App、没有小程序),而且后端接口已经能够很好地满足前端的数据需求,这时候引入 BFF 只会增加一个需要维护的服务。没有数据不匹配的问题,就没有 BFF 需要解决的矛盾。

如果团队的后端接口本身就是按页面需求组织的(这种情况在中小型团队里并不少见),那么 BFF 的聚合功能已经被后端 API 实现了,重复放在 BFF 里只是增加维护成本。

如果你的系统规模很小,团队只有两三个人,前后端沟通成本本来就很低,那么 BFF 增加的抽象层次可能还不如直接让前端调用多个后端接口来得简单。架构是为团队规模服务的,过度架构化反而会拖累小团队的速度。

BFF 对团队协作方式的影响

BFF 真正发挥作用的前提,是前端团队对 BFF 有所有权或者强协作权。

在传统的协作模式里,前端工程师提接口需求,后端工程师评审和实现,双方通过文档和会议对齐契约。这个过程容易陷入反复:前端觉得后端返回的字段太多或太少,后端觉得前端的需求总是变,双方在接口设计上花费大量沟通成本。接口变更需要跨团队协调,短则几天,长则几周。

如果前端团队拥有或深度参与 BFF 的开发,协作方式会变成:前端设计师和产品经理讨论完页面需求后,直接和 BFF 工程师(可能是同一个人)确定数据契约,BFF 工程师再决定需要调用哪些下游服务。这个过程中,前端对接口契约有直接的控制权,不需要通过跨团队沟通去推动变更。

这种协作方式在多端场景下尤其有价值。同一个产品同时服务 Web、iOS、Android、小程序时,每个端的数据需求不同、迭代节奏不同。如果强依赖统一的通用 API,每个端都要在其他端的变更影响下工作,变更协调成本极高。每个端有自己独立的 BFF 之后,端之间的耦合降低,各自可以按自己的节奏迭代。

但这里有一个常见的陷阱。如果 BFF 属于后端平台团队,而不是前端团队,那么前端还是要走跨团队沟通流程,BFF 的交付效率优势就会大打折扣。所以选择 BFF 模式的同时,也要选择相应的团队结构——要么让前端团队 owning BFF,要么让 BFF 团队和前端团队形成紧密的共生关系。

缓存是 BFF 常被低估的能力

大多数团队想到 BFF 时,首先考虑的是聚合功能,缓存往往被放在次要位置。但 BFF 其实是一个非常适合做缓存的层级。

原因在于这三个事实:BFF 收到的请求已经包含完整的用户上下文,可以在缓存层面做用户级别的数据隔离;BFF 返回的是前端页面可以直接使用的数据,缓存命中后不需要任何二次处理;BFF 作为数据聚合层,缓存命中率的计算比缓存单个下游接口更简单和可靠。

常见的缓存策略有几种。页面级聚合缓存适合数据变更不频繁的场景,比如商品详情页,可以设置几分钟到十几分钟的 TTL,配合 CDN 在边缘节点缓存,用户就近获取数据减少延迟。用户在上下文相关的缓存适合基于用户身份个性化的数据,比如用户专属的推荐列表,但要注意退出登录时的缓存清理。stale-while-revalidate 策略适合对数据新鲜度有一定容忍度的场景,系统先返回过期数据给用户,同时后台异步获取新数据,用户下次访问时拿到更新后的内容。

需要特别注意的是缓存安全。BFF 层面最容易犯的错误是缓存了包含用户个性化信息的数据,但没有设置合理的缓存 key 或者 TTL 管理,导致用户 A 的数据被返回给用户 B。

// 错误的缓存方式:缓存 key 不包含用户信息
const cacheKey = `product:${productId}`;
const cached = await cache.get(cacheKey);
if (cached) return cached; // 用户 A 的数据可能被返回给用户 B

// 正确的缓存方式:缓存 key 包含用户上下文
const cacheKey = `product:${productId}:user:${userId}:region:${region}`;
const cached = await cache.get(cacheKey);
if (cached) return cached;

BFF 的测试策略

BFF 的测试策略和普通后端服务有所不同,因为它既涉及下游服务的集成,又有前端契约的责任。

单元测试主要覆盖 BFF 自己的编排逻辑:字段裁剪是否正确、聚合逻辑是否按预期工作、用户上下文注入是否准确。由于 BFF 的逻辑相对简单(主要是编排而不是复杂计算),单元测试覆盖率可以比较高。

集成测试的重点是验证 BFF 和下游服务之间的交互:当下游服务返回正常数据时 BFF 是否正确处理、当下游服务返回错误时 BFF 的降级策略是否合理、当下游服务超时 BFF 是否能正确处理。需要注意的是,集成测试应该使用真实的下游服务或者可靠的 mock 服务,而不是 mock 所有东西——BFF 的价值正在于连接多个真实服务,如果全部 mock 掉了,就测不到真实的组合逻辑。

端到端测试验证完整的请求链路:从前端发起请求、到 BFF 接收处理、到调用下游服务、到返回聚合结果。但端到端测试的维护成本较高,通常只覆盖核心业务路径。

契约测试(在 BFF 和下游服务之间)可以确保当下游接口变更时,BFF 能够及时发现不兼容的改动。这是一个经常被忽视但很有价值的测试层次。

常见误解

第一种误解是把 BFF 等同于 API Gateway。两者的职责定位完全不同,前面已经详细说明。

第二种误解是觉得 BFF 只是多加了一层 HTTP 调用,没有实际价值。如果只是做请求转发,不做聚合和塑形,那确实只是在增加网络延迟。真正的 BFF 做的是数据编排工作,把多个下游服务的结果组合成前端可以直接使用的数据结构,这个组合工作是真实存在的认知劳动。

第三种误解是把技术选型和架构模式混为一谈。用了 GraphQL 不等于做了 BFF,用了 tRPC 也不等于做了 BFF。关键要看契约是不是由前端需求驱动的、边界职责是不是清晰的。

第四种误解是觉得 BFF 越强大越好。把核心业务逻辑往 BFF 里塞,短期内交付快,但 BFF 的边界一旦模糊,就会从一个「数据适配层」变成「新型业务单体」,给系统长期维护带来隐患。

第五种误解是觉得 BFF 只能用于多端场景。单端场景下,如果前后端的数据契约经常不匹配、变更协调成本高,BFF 依然有价值。单端 BFF 解决的是「谁拥有接口契约」的问题,不只是「多个端需要不同数据」的问题。

面试中怎么表达 BFF

一个展示深度的 BFF 回答应该包含几个层面。首先解释清楚 BFF 解决的是什么问题——不是简单的一层转发,而是解决后端服务的数据结构和前端页面实际需要的数据之间的不匹配。然后说明 BFF 和 API Gateway 的本质区别,前者处理前端体验层面的数据聚合,后者处理平台层面的横切策略。接着可以谈谈实际工程中的判断标准,什么逻辑应该放在 BFF、什么逻辑应该留在领域服务——这里可以给出一个具体的判断思路,比如「如果这个逻辑移走了前端调用 BFF 的方式会不会变」。最后可以结合自己的经历谈谈 BFF 在团队协作层面带来的变化,比如契约所有权的改变如何影响了团队交付节奏。

延展阅读

  • Sam Newman — Backends for Frontends — BFF 模式的原始提出者定义,内容经过多年依然是理解这个模式最好的起点。Sam Newman 后来在《构建微服务》一书中也多次引用这个模式。
  • Microsoft Azure — BFF Pattern — 微软官方的 BFF 模式说明,从企业架构角度分析了适用场景和实现注意事项,特别是多客户端场景下的决策框架。
  • tRPC 官方文档 — 端到端类型安全的 BFF 实现方案,适合同一团队维护前后端边界的场景,文档中对类型推断的实现原理有详细说明。
  • GraphQL 官方文档 — GraphQL 本身是工具,用 GraphQL 实现了 BFF 不等于理解了 BFF,但了解 GraphQL 的能力边界对架构决策有帮助。
  • SoundCloud 官方技术博客 — SoundCloud 是最早公开分享 BFF 实践的公司之一,他们描述了如何在从单体向微服务迁移的过程中使用 BFF 模式解决前端和后端的集成问题。

BFF 引入前的评估框架

在决定是否引入 BFF 之前,可以用一个简单的评估框架来判断:

flowchart TD
    A["后端接口是否<br/>满足前端需求?"] --> B{是否}
    B -->|是,单端| C["当前架构足够<br/>暂不需要 BFF"]
    B -->|否,单端| D["评估 BFF 引入成本<br/>对比契约协调成本"]
    D --> E{成本划算?}
    E -->|是| F["引入 BFF"]
    E -->|否| G["优化后端接口或<br/>接受前端聚合"]
    C --> Z[结束]
    F --> Z
    G --> Z

这个框架的核心问题是:数据不匹配的痛苦和 BFF 引入成本的痛苦,哪个更大?没有 BFF 也能活下去的情况下,不必为了「架构正确」引入不必要的复杂度。

BFF 的运维考量

引入 BFF 之后,需要考虑的不只是开发层面,还有运维层面的变化。

BFF 是多个下游服务的对外窗口,这意味着 BFF 的可用性直接影响前端体验。当下游某个服务变慢时,BFF 的响应也会变慢;当某个下游服务完全不可用时,BFF 必须决定是返回部分数据还是整体报错。这些决策需要提前想清楚,而不是在故障发生时现场决定。

一个实用的做法是为每个下游服务设置合理的超时时间,并在 BFF 层面实现降级策略。比如评价服务不可用时,商品详情页可以返回「评价加载中」而不是完全不能展示商品。这是 BFF 作为聚合层的额外价值——它可以在部分下游失败时提供有损服务,而不是整体失败。

监控方面,除了常规的接口 QPS、延迟、错误率,还需要关注下游服务的健康状态。一个 BFF 的监控面板应该能让人一眼看到:当前有哪些下游服务可用、哪些服务的响应时间变长了、前端收到的错误有多少比例是来自哪个下游服务。

BFF 和 SSR/SSG 的关系

在讨论前端架构时,BFF 经常和 Server-Side Rendering(SSR)以及 Static Site Generation(SSG)一起被提到。它们解决的是不同维度的问题,但在实际项目中经常配合使用。

SSR 解决的是首屏渲染速度和 SEO 问题——页面在服务器端渲染完成后返回给浏览器,用户能更快看到内容。SSR 返回的 HTML 里面通常包含初始数据,这些数据从哪里来?可以是 SSR 框架直连后端服务,也可以是 SSR 服务调用 BFF。通常来说,如果页面需要聚合多个下游服务的数据,让 SSR 层调用 BFF 比让 SSR 直连多个下游更合理——BFF 已经处理好了数据聚合逻辑,SSR 层不需要关心这些细节。

SSG 适合内容不频繁变更的页面,比如商品详情页这种「一天更新一次」的页面。SSG 在构建时生成静态 HTML,同时生成对应的 JSON 数据文件供前端 hydration 使用。这里的数据文件生成过程,和 BFF 的数据聚合逻辑可以是同一套——构建时 BFF 拉取数据生成静态文件,运行时不经过 BFF 直接返回静态内容。这种模式下 BFF 和 SSG 各司其职:BFF 负责「如何聚合数据」,SSG 负责「什么时候生成数据」。

一个实际的 BFF 演进路径

很多团队不是一开始就决定用 BFF,而是随着系统复杂度增长逐步演进过来的。

第一阶段是「前端直连多个服务」,团队小、系统简单,前端直接调用需要的下游服务,变更协调成本可接受。第二阶段是「前端开始抱怨」,随着团队扩大、前端增多,下游接口一变影响多个前端,跨团队协调开始成为瓶颈。第三阶段是「引入 BFF」,前端团队(或者全栈团队)开始 owning 一个专门处理聚合的层,契约由前端需求驱动。第四阶段是「多端 BFF」,当产品扩展到多个前端类型时,各自的 BFF 开始独立演进,共享的逻辑下沉到下游服务。

这个演进路径是自然的,不需要一开始就想清楚「我们将来会有 5 个不同的客户端」。当第一阶段的痛苦变得不可接受时,BFF 的引入就是合理的。

技术债务和 BFF

很多遗留系统的前端部分积累了大量「前端自己处理数据转换」的代码,这些代码散落在各个前端项目里,没有人敢动,因为不知道动了会不会影响其他页面。

BFF 可以作为一个清理这些技术债务的契机。把散落在前端的数据转换逻辑逐步迁移到 BFF,统一管理、统一测试。前端不再需要知道「这个价格字段叫 price 但那个接口里叫 amount」,这些映射关系在 BFF 统一处理。

但这个迁移过程要谨慎。最好的做法是:先在 BFF 实现新的接口,让新的前端页面使用新接口,老接口保持不动;等新接口稳定之后,再逐步把老前端迁移过来;完全迁移完成之后,再下线老接口。这是一个以 BFF 为中心的数据契约重构过程,风险可控。