BFF 架构实践指南
最近团队在推进 BFF 拆分,借这个机会把相关概念、落地方式和取舍整理成一份文档,供讨论和后续开发参考。
一、BFF 是什么
BFF(Backend-for-Frontend,面向前端的后端)是一种架构模式。核心思想是:为不同的前端客户端(Web、App、小程序)提供专属的后端适配层,解决前后端分离架构中普遍存在的数据聚合和多端接口适配问题。

它主要做三件事:数据剪裁、接口聚合、响应格式适配。
1.1 与 API 网关的区别
这是实际拆分中最容易混淆的点,先划清边界:
| 维度 | API 网关 | BFF |
|---|---|---|
| 定位 | 流量基础设施层 | 业务数据编排层 |
| 核心职责 | 路由、统一鉴权、限流、SSL、协议转换、黑白名单 | 数据聚合、字段裁剪、多端差异化返回 |
| 业务感知 | 不感知具体业务 | 感知前端页面和客户端差异 |
| 部署粒度 | 通常全局一个 | 可按端或按业务域拆多个 |
简单说:网关管"流量能不能进来、怎么分发",BFF 管"进来之后的数据怎么组装成前端想要的样子"。两者职责不能上下乱迁移。
1.2 主要功能
- 数据聚合 :并行调用多个下游微服务,将分散数据整合为前端所需结构,避免前端串行发起大量 HTTP 请求,也消除了后端为了兼容多端而返回大而全对象的问题。
- 多端定制 API:根据 Web / App / 小程序不同的展示需求和流量约束裁剪字段,消除数据冗余。
- 统一异常处理 :集中捕获下游异常,标准化 HTTP 状态码和错误结构,对外屏蔽内部服务异常细节。
- 短时缓存:对聚合结果做短时间缓存,降低下游并发压力,优化响应耗时。仅适用于弱一致性、读多写少的数据。
- 调用编排与容错:统一管理下游调用的超时、重试、降级策略。
1.3 优势
- 降低前端复杂度:前端只对接 BFF,无需维护大量微服务调用逻辑。
- 天然适配多端:一套底层领域服务,通过 BFF 快速实现多端差异化接口,不需要改动核心服务。
- 迭代灵活:前端展示层需求变更优先在 BFF 调整,减少核心领域服务的发布频次。
- 性能优化集中:聚合、缓存、请求合并统一在该层落地,便于治理。
二、核心链路

三、代码实践
BFF 一般独立部署为 Spring Boot 应用,典型场景有两类:多端接口适配、跨服务数据聚合。
3.1 接口适配与数据裁剪
同一个订单接口,App 端只需要精简字段省流量,Web 端需要完整字段支撑复杂页面:
java
// App 端 BFF:精简返回
@RestController
@RequestMapping("/app/orders")
public class AppOrderBffController {
@Autowired
private OrderService orderService;
@GetMapping
public AppOrderVO getOrders(@RequestHeader("X-User-ID") String userId) {
OrderDTO orderDTO = orderService.getOrders(userId);
return AppOrderConverter.convert(orderDTO);
}
}
// Web 端 BFF:返回完整数据
@RestController
@RequestMapping("/web/orders")
public class WebOrderBffController {
@Autowired
private OrderService orderService;
@GetMapping
public WebOrderVO getOrders(@CookieValue("SESSION_ID") String sessionId) {
OrderDTO orderDTO = orderService.getOrders(sessionId);
return WebOrderConverter.convert(orderDTO);
}
}
3.2 数据聚合的两种方式
BFF 层的核心能力是并行聚合,避免串行调用导致响应时间累加。
方式一:CompletableFuture(同步 Spring Boot 项目常用)
java
@Service
public class ProductDetailBffService {
@Autowired
private ProductService productService;
@Autowired
private PriceService priceService;
@Autowired
private InventoryService inventoryService;
@Autowired
private RecommendService recommendService;
// 生产环境必须使用自定义线程池,不要用默认 ForkJoinPool.commonPool()
@Autowired
private ExecutorService bffThreadPool;
public ProductDetailVO getProductDetail(String productId, String userId) {
// 并行发起下游调用
CompletableFuture<ProductInfo> productFuture = CompletableFuture.supplyAsync(
() -> productService.getProductInfo(productId), bffThreadPool);
CompletableFuture<PriceInfo> priceFuture = CompletableFuture.supplyAsync(
() -> priceService.getPrice(productId), bffThreadPool);
CompletableFuture<InventoryInfo> inventoryFuture = CompletableFuture.supplyAsync(
() -> inventoryService.getInventory(productId), bffThreadPool);
CompletableFuture<List<RecommendItem>> recommendFuture = CompletableFuture.supplyAsync(
() -> recommendService.getRecommendations(userId, 6), bffThreadPool);
// 整体超时控制
CompletableFuture.allOf(productFuture, priceFuture, inventoryFuture, recommendFuture)
.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(ex -> null)
.join();
// 单个服务失败时降级,不阻塞整体返回
return ProductDetailVO.builder()
.product(getWithDefault(productFuture, null))
.price(getWithDefault(priceFuture, null))
.inventory(getWithDefault(inventoryFuture, null))
.recommendations(getWithDefault(recommendFuture, Collections.emptyList()))
.build();
}
private <T> T getWithDefault(CompletableFuture<T> future, T defaultValue) {
try {
return future.isCompletedExceptionally() ? defaultValue : future.get();
} catch (Exception e) {
return defaultValue;
}
}
}
两个注意点:
- 必须指定线程池 。
supplyAsync不传入 Executor 时默认使用ForkJoinPool.commonPool(),这是 JVM 全局共享的,BFF 高并发场景下极易被打满,影响整个进程。 - 单服务降级。某个下游超时或报错时,应返回默认值(如推荐列表返回空),而不是让整个接口失败。
方式二:响应式编程 Project Reactor(高并发非阻塞场景)
java
@Service
public class ProductBffService {
@Autowired
private ProductClient productClient;
@Autowired
private InventoryClient inventoryClient;
@Autowired
private PromotionClient promotionClient;
public Mono<ProductDetailVO> getProductDetail(String productId) {
return Mono.zip(
productClient.getProduct(productId),
inventoryClient.getStock(productId),
promotionClient.getPromotion(productId)
).map(tuple -> assembleVO(tuple.getT1(), tuple.getT2(), tuple.getT3()));
}
}
Reactor 基于非阻塞 I/O 和事件驱动模型,适合高并发聚合场景,但要求全链路(包括下游 Client)都是响应式的,否则混合使用反而会阻塞事件循环。
四、避坑指南
4.1 避免代码复制
不同 BFF 之间的公共逻辑(用户鉴权、统一日志、通用转换器、异常处理)应下沉到公共 SDK,禁止每个 BFF 项目复制粘贴相同代码。
4.2 避免过度聚合
单个 BFF 接口不建议聚合过多下游微服务。下游依赖过多,链路极易超时,任何一个服务抖动都会拖慢整体响应。遵循单一职责,拆成多个接口或分步加载。
4.3 先评估必要性
业务简单、客户端类型单一的场景,额外维护一层 BFF 只会增加运维成本和对象转换代码。一个设计良好的通用 API 可能就够了,不要为了架构而架构。
4.4 职责清晰有边界
- Web BFF 专注处理复杂嵌套对象、完整字段返回;
- App BFF 注重数据裁剪、省流量、合并请求;
- BFF 项目之间互不干涉,独立部署,禁止相互远程调用。
五、落地约束
5.1 严格边界
BFF 的定位是数据编排、转换、聚合,以下事情不应该出现在 BFF 层:
- 不实现核心业务规则和复杂领域计算;
- 不写数据库事务,不直接操作数据库;
- 不承载领域核心逻辑;
- 不做网关该做的限流、全局路由。
边界一旦模糊,BFF 会逐步膨胀成第二个臃肿的业务服务,造成架构腐化。
5.2 拆分方案
常见两种拆分模式,团队拆分前需要统一选择:
| 拆分方式 | 优势 | 劣势 |
|---|---|---|
| 按端拆分(WebBFF / AppBFF / 小程序BFF) | 天然支持多端差异,各端独立迭代 | 相同业务聚合逻辑容易重复 |
| 按业务域拆分(商品BFF / 订单BFF) | 逻辑复用性好,业务内聚 | 多端差异化处理不够直观 |
5.3 反模式
- 在 BFF 中编写核心业务计算、复杂领域规则;
- BFF 直接操作数据库;
- BFF 之间互相远程调用;
- 将网关限流、全局路由等基础设施逻辑下沉至 BFF;
- 一个接口聚合十几个下游服务,且无降级预案。
六、Trade-off 讨论
6.1 导入导出走 BFF 吗
对于超大文件:
- BFF 仅负责接收请求、下发异步任务、返回 taskId;
- 核心后端异步处理数据并写入 OSS;
- 前端拿到链接后直接从 OSS 下载,海量数据流量不经过 BFF。
这样做的好处是:导入导出极耗资源,BFF 独立部署可以防止大文件处理导致核心服务 OOM 崩溃。
如果存在统一权限校验、请求参数统一收口的诉求,可以保留请求经过 BFF,但不要让 BFF 参与文件 IO。
6.2 多一层有必要吗
多一层应用,意味着多一层数据转换,代码中会充斥大量 DTO / VO 转换,部署和运维成本也会增加。这不是免费的。
引入 BFF 的判定参考,满足多条时再考虑:
- 存在多端客户端,且各端数据需求差异明显;
- 前端请求零散,一个页面需要调用多个后端接口;
- 频繁需要字段裁剪(尤其是移动端省流量场景);
- 页面需要跨多个服务组装数据。
如果业务简单、客户端类型单一、迭代节奏慢,额外维护一层 BFF 是得不偿失的。
七、总结
回到架构的本质:能满足当前业务场景、团队能维护得住的架构,就是好架构。BFF 不是银弹,大厂的方案也不一定适合我们的阶段。拆分之前先想清楚:痛点到底是多端适配,还是接口聚合,如果只是其中一个,也许一个轻量的适配层就够了,不必上完整的 BFF 体系。盲目套用大厂经验,容易陷入经验主义的误区。