BFF 架构实践指南

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;
        }
    }
}

两个注意点:

  1. 必须指定线程池supplyAsync 不传入 Executor 时默认使用 ForkJoinPool.commonPool(),这是 JVM 全局共享的,BFF 高并发场景下极易被打满,影响整个进程。
  2. 单服务降级。某个下游超时或报错时,应返回默认值(如推荐列表返回空),而不是让整个接口失败。
方式二:响应式编程 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 体系。盲目套用大厂经验,容易陷入经验主义的误区。

相关推荐
sunywz2 小时前
【高并发秒杀架构】(2)通用秒杀架构梳理
架构
ltl2 小时前
架构的不变量:穿越技术周期的设计原则
架构
西门老铁5 小时前
AI 时代高效画图的方案——AIGC+PlantUML
架构
这token有力气5 小时前
3000行全塞一个 index.html?四刀瘦身手术,不装 Vite 也能模块化
架构
刘立军5 小时前
领域驱动设计:给 AI 划定上下文边界,告别“大泥球”代码
架构·ai编程·领域驱动设计
用户6919026813396 小时前
用浏览器 WebGPU跑DPSK大模型(1) - 模型的下载和前端下载进度的显示
javascript·react.js·架构
xiaoshuai10246 小时前
编译管不着的跨层 bug:用 4 道脚本闸守住 API/SQL/权限/迁移
架构
Dr.kangder7 小时前
嵌入式面试总结(八)——大小端
嵌入式硬件·面试·职场和发展·架构·嵌入式
李白客7 小时前
分布式集群与数据库产业:从单机到集群的架构跃迁与市场重构
数据库·分布式·架构