Spring Boot 3.5 + Java 25 + Cloud Native 系列(三):BFF 聚合层
目录
在上一篇中,我们看了 API Gateway 如何作为统一入口处理 JWT 校验和路由分发。请求经过 Gateway 后,到达的就是本文的主角——BFF(Backend for Frontend)聚合层。
📦 本文基于的完整项目源码:https://github.com/meirongdev/shop
2026-04 实践更新 当前主线代码里,buyer-bff / seller-bff 都已经完成了 typed
@HttpExchange客户端改造;BuyerAggregationService/SellerAggregationService不再直接持有一堆 rawRestClient调用链,而是通过HttpServiceProxyFactory注入声明式客户端。本文下面与客户端装配相关的表述已按当前实现校正。
BFF 模式:为什么需要聚合层#
在微服务架构中,前端页面往往需要从多个领域服务获取数据。如果没有 BFF,前端直接调用每个领域服务会面临:
前端 ←→ profile-service (获取用户档案)
←→ wallet-service (获取钱包余额)
←→ promotion-service (获取可用优惠)
←→ marketplace-service (获取推荐商品)
←→ loyalty-service (获取积分账户)
问题显而易见:前端需要知道每个服务的地址、认证方式、数据格式;任何服务变动都会影响前端代码;更重要的是,前端暴露了内部服务拓扑,安全和治理成本都会明显上升。
BFF 在这里提供了一个位于前端和领域服务之间的聚合层:
前端 ←→ BFF ←→ profile-service
←→ wallet-service
←→ promotion-service
←→ marketplace-service
←→ loyalty-service
前端只需要和一个 BFF 打交道,BFF 负责编排、聚合、容错、数据转换。
整体架构#
Shop Platform 有两个 BFF:
| BFF | 服务对象 | 核心职责 |
|---|---|---|
| buyer-bff | 买家/游客 | Dashboard、购物车、结账、订单追踪、忠诚度中心 |
| seller-bff | 卖家 | 商品管理、订单管理、促销配置 |
两者结构相似,以 buyer-bff 为例深入分析。
包结构#
buyer-bff/
├── config/
│ ├── BuyerBffConfig.java # @HttpExchange proxy + HttpClient 装配
│ ├── BuyerClientProperties.java # @ConfigurationProperties(目标服务地址、超时)
│ └── OpenApiConfig.java
├── controller/
│ ├── BuyerController.java # 已登录买家端点
│ └── GuestBuyerController.java # 游客端点(结账、订单追踪)
└── service/
├── BuyerAggregationService.java # 核心聚合逻辑(typed clients + 编排 / 降级 / 补偿)
├── GuestCartStore.java # Redis 游客购物车
└── PageResponse.java # 分页响应包装
Virtual Threads 并发编排#
BFF 最常见的场景是并发调用多个下游服务,然后组合结果返回。在 WebFlux 时代这是 Mono.zip(...),在 Virtual Threads 时代变成了最朴素的 Future.get():
public BuyerApi.DashboardResponse loadDashboard(String buyerId) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var profileFuture = executor.submit(() ->
call("profileService", true,
() -> profileClient.getProfile(buyerId),
"Profile service unavailable"));
var walletFuture = executor.submit(() ->
call("walletService", true,
() -> walletClient.getAccount(buyerId),
"Wallet service unavailable"));
var promotionsFuture = executor.submit(() ->
call("promotionService", true,
() -> promotionClient.listOffers(),
"Promotions unavailable"));
var marketplaceFuture = executor.submit(() ->
call("marketplaceService", true,
() -> marketplaceClient.listProducts(),
"Marketplace unavailable"));
var loyaltyFuture = executor.submit(() ->
call("loyaltyService", true,
() -> loyaltyClient.getAccount(buyerId),
"Loyalty service unavailable"));
return new BuyerApi.DashboardResponse(
profileFuture.get(), walletFuture.get(), promotionsFuture.get(),
marketplaceFuture.get(), loyaltyFuture.get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException(CommonErrorCode.INTERNAL_ERROR,
"Buyer dashboard interrupted", e);
} catch (ExecutionException e) {
throw new BusinessException(CommonErrorCode.DOWNSTREAM_ERROR,
"Buyer dashboard aggregation failed", e);
}
}
关键点:
- try-with-resources:每次请求创建新的
VirtualThreadPerTaskExecutor,方法结束时自动关闭 - 5 个并发调用:profile、wallet、promotions、marketplace、loyalty,各自运行在独立虚拟线程上
- 同步等待:
.get()阻塞的是虚拟线程而非操作系统线程,开销极低 - 异常处理:
InterruptedException恢复中断标记,ExecutionException包装为统一业务异常
这种写法的代码可读性和传统同步代码比较接近,但运行时仍然是并发的。对习惯同步调用栈的团队来说,断点调试和异常定位通常也会更直观一些。
Resilience4j:四层防护#
上面的 call() 方法实际上封装了 Resilience4j 的四层防护:
private <T> T call(String instanceName, boolean retryEnabled,
Supplier<T> supplier, String unavailableMessage) {
if (retryEnabled) {
return resilienceHelper.read(instanceName, supplier,
throwable -> failDownstream(unavailableMessage, throwable));
}
return resilienceHelper.write(instanceName, supplier,
throwable -> failDownstream(unavailableMessage, throwable));
}
ResilienceHelper(来自 shop-common)将四个 Resilience4j 组件串联成一个装饰器链:
// 装饰器链顺序:Retry → CircuitBreaker → Bulkhead → TimeLimiter
Supplier<T> retryDecorated = retryEnabled ?
Retry.decorateSupplier(retry, supplier) : supplier;
Supplier<T> circuitDecorated =
CircuitBreaker.decorateSupplier(circuitBreaker, retryDecorated);
Supplier<T> bulkheadDecorated =
Bulkhead.decorateSupplier(bulkhead, circuitDecorated);
return timeLimiter.executeFutureSupplier(
() -> executorService.submit(bulkheadDecorated::get));
各服务防护参数矩阵#
CircuitBreaker(熔断器):
| 下游服务 | 滑动窗口大小 | 失败率阈值 | 开启等待时间 | 半开允许请求数 |
|---|---|---|---|---|
| searchService | 10 | 50% | 30s | 3 |
| promotionService | 20 | 50% | 20s | 3 |
| loyaltyService | 20 | 50% | 20s | 3 |
| marketplaceService | 20 | 50% | 10s | 5 |
| orderService | 10 | 40% | 15s | 2 |
| profileService | 20 | 60% | 30s | 5 |
| walletService | 20 | 50% | 20s | 3 |
Retry(重试,仅读操作):
| 下游服务 | 最大尝试次数 | 初始等待时间 | 指数退避 |
|---|---|---|---|
| searchService | 3 | 200ms | 是 (x2) |
| promotionService | 2 | 300ms | 否 |
| marketplaceService | 2 | 200ms | 否 |
| profileService | 2 | 200ms | 否 |
重试只针对 ResourceAccessException / ConnectException,BusinessException 不重试。
Bulkhead(信号量隔离):
| 下游服务 | 最大并发 | 最大等待 |
|---|---|---|
| searchService | 20 | 50ms |
| marketplaceService | 15 | 0ms (快速失败) |
| profileService | 15 | 0ms |
| promotionService | 10 | 0ms |
| loyaltyService | 10 | 0ms |
| orderService | 8 | 0ms |
| walletService | 8 | 0ms |
TimeLimiter(超时):
| 下游服务 | 超时时间 | 取消运行中任务 |
|---|---|---|
| searchService | 5s | 是 |
| promotionService | 4s | 是 |
| profileService | 4s | 是 |
| marketplaceService | 3s | 是 |
| orderService | 3s | 是 |
| walletService | 3s | 是 |
| loyaltyService | 4s | 是 |
为什么读操作开启重试,写操作不开#
- 读操作:重试不会导致数据副作用(获取商品列表、查询用户档案)
- 写操作:创建订单、支付、扣库存等写操作如果重试,可能造成重复下单、重复扣款,因此写操作只执行一次,失败直接返回
RestClient 声明式 HTTP 客户端#
BFF 调用下游服务使用的是 Spring 6.1 引入的 RestClient + @HttpExchange:
// 声明式客户端接口
@HttpExchange
public interface SearchServiceClient {
@GetExchange("/api/search/products")
SearchApi.SearchResponse searchProducts(@RequestParam("q") String query,
@RequestParam("page") int page);
}
// 配置类中装配
@Bean
SearchServiceClient searchServiceClient(BuyerClientProperties properties,
JdkClientHttpRequestFactory factory) {
RestClient searchRestClient = RestClient.builder()
.requestFactory(factory)
.baseUrl(properties.searchServiceUrl())
.build();
HttpServiceProxyFactory httpFactory = HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(searchRestClient))
.build();
return httpFactory.createClient(SearchServiceClient.class);
}
对比 RestTemplate,@HttpExchange 在这个项目里带来的几个变化:
- 接口定义即文档,参数和返回类型一目了然
- 编译期类型检查,不需要在运行时拼字符串
- 天然支持虚拟线程(运行在
RestClient之上)
另外,这套 RestClient 会自动吃到 shop-common 里的 HeaderPropagationAutoConfiguration,把 Gateway 注入的 X-Request-Id、X-Buyer-Id、X-Username、X-Roles、X-Portal 等可信 Header 继续往下游透传。
为什么选择 @HttpExchange 而不是 OpenFeign?#
在微服务架构中,声明式客户端(declarative HTTP client)是 BFF 调领域服务的标准方式。早期的 shop 项目尝试过 Spring Cloud OpenFeign,后来整体迁移到了 Spring Framework 原生的 @HttpExchange + RestClient。选择后者主要基于几点考量:
- Spring Cloud OpenFeign 已进入维护模式。Spring Cloud 2022.0.0 之后,OpenFeign 不再作为 Spring Cloud 默认集成的 HTTP 客户端,Spring 官方转向了基于 Spring Framework 6.1+ 原生 HTTP Interfaces 的路线。继续使用 OpenFeign 意味着承担生态滞后的风险。
@HttpExchange是 Spring Framework 内置能力,直接存在于spring-web模块中,不需要引入额外的 Spring Cloud 组件。对于部署在 Kubernetes 上的服务,K8s 自身的 Service 机制已经处理了服务发现和负载均衡,引入 Spring Cloud 全套治理组件反而成了不必要的重量依赖。- 与 Java 21 虚拟线程原生兼容。
RestClient为虚拟线程做了原生优化,配合@HttpExchange可以在不写响应式代码的前提下,以极低的硬件成本获得高并发吞吐。OpenFeign 的同步阻塞模型在高并发下更容易耗尽线程池。 - 底层 HTTP 引擎统一。无论是直接使用
RestClient编写编程式调用,还是用@HttpExchange编写声明式接口,它们共享同一套拦截器、HttpMessageConverter、连接池配置和错误处理机制。
注解层面的迁移很简单,基本上是一一对应:
| 功能 | OpenFeign | @HttpExchange |
|---|---|---|
| 声明客户端 | @FeignClient(name = "user-service") |
@HttpExchange("/users") 或通过工厂配置 |
| GET 请求 | @GetMapping |
@GetExchange |
| POST 请求 | @PostMapping |
@PostExchange |
| 路径参数 | @PathVariable |
@PathVariable(无变化) |
| 请求体 | @RequestBody |
@RequestBody(无变化) |
以用户服务为例,迁移前后的接口声明对比:
// 迁移前:OpenFeign
@FeignClient(name = "user-service", url = "https://api.example.com/users")
public interface UserFeignClient {
@GetMapping("/{id}")
UserResponse getUserById(@PathVariable("id") Long id);
@PostMapping
UserResponse createUser(@RequestBody CreateUserRequest request);
}
// 迁移后:@HttpExchange
@HttpExchange("/users")
public interface UserHttpClient {
@GetExchange("/{id}")
UserResponse getUserById(@PathVariable("id") Long id);
@PostExchange
UserResponse createUser(@RequestBody CreateUserRequest request);
}
配置层的变化也类似:OpenFeign 靠 @EnableFeignClients 自动扫描并注册 Bean,而 @HttpExchange 需要显式构造 HttpServiceProxyFactory。不过这个样板代码可以被 ShopHttpExchangeSupport 这类统一工厂封装掉——每个 caller 的 @Bean 方法通常只有 3-5 行。
两层超时机制#
BFF 有两个维度的超时设置,它们是不同的防护层:
| 层级 | 配置 | 当前默认值 | 说明 |
|---|---|---|---|
| HTTP 客户端层 | connectTimeout / readTimeout |
2s / 5s | JDK HttpClient 级别 |
| Resilience4j TimeLimiter | 各服务独立配置 | 3-5s | 业务编排级别 |
HTTP 客户端超时是底层网络超时(TCP 连接 + 读取响应),TimeLimiter 是整个 call() 方法的执行超时(包含重试、熔断判断等逻辑)。两层组合提供了更细粒度的控制。
buyer-bff vs seller-bff 的 HttpClient 差异#
一个有趣的细节:buyer-bff 默认使用 HTTP/1.1,而 seller-bff 硬编码 HTTP/2:
// buyer-bff: HTTP/1.1(默认)
HttpClient.newBuilder()
.version(properties.httpVersion()) // HTTP_1_1
.connectTimeout(properties.connectTimeout())
.build();
// seller-bff: HTTP/2
HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // 强制 HTTP/2
.connectTimeout(properties.connectTimeout())
.build();
这里更适合把它理解为当前实现里的差异化取舍:seller-bff 目前固定使用 HTTP/2,buyer-bff 则保留可配置、默认 HTTP/1.1。它可能和卖家侧更偏后台操作、接口调用模式更集中有关,但如果没有结合压测数据,不宜把这点直接上升成通用最佳实践。
结账流程:Saga 编排 + 优雅降级#
buyer-bff 中最复杂的业务逻辑是结账(checkout)。它需要在多个领域服务之间编排一个同步 Saga:
① 获取购物车
② 按卖家分组商品
③ 预扣库存(marketplace-service)
④ 为每个卖家创建订单(order-service)
⑤ 处理支付(wallet-service / Stripe)
⑥ 扣减积分(loyalty-service,可选)
⑦ 清空购物车
⑧ 返回支付结果
任何一步失败都需要逆向补偿:
补偿策略:
- 逆向顺序:先恢复库存、再退款、最后退回积分(与正向操作相反)
- Fire-and-forget:每个补偿操作独立 try-catch,失败只记录日志不抛异常(避免补偿本身失败把原始异常完全淹没)
- 持久化补偿任务:对于关键补偿(如库存恢复),marketplace-service 有
CompensationTaskEntity持久化重试机制(指数退避,最大 5 次)
这里也要说清楚边界:当前 buyer-bff 里的补偿仍然偏应用层编排,适合说明思路,但还不是一个“所有补偿都可追踪、可审计、可人工介入”的完整工作流系统。
优雅降级:优惠不可用 ≠ 结账不可用#
结账流程中,某些依赖服务失败不应阻止用户完成购买:
优惠券验证降级:
PromotionApi.CouponValidationResponse couponValidation = null;
if (request.couponCode() != null && !request.couponCode().isBlank()) {
couponValidation = validateCouponForCheckout(request.couponCode(), cart.subtotal());
// 如果 promotion-service 不可用,couponValidation 为 null
// 结账继续进行,只是不使用优惠券折扣
}
public PromotionApi.CouponValidationResponse validateCouponForCheckoutFallback(
String couponCode, BigDecimal orderAmount, Throwable throwable) {
if (throwable instanceof BusinessException businessException
&& shouldPropagateCouponFailure(businessException)) {
throw businessException; // 业务错误(券无效/已过期)→ 硬失败
}
log.warn("promotion-service unavailable, skipping coupon {}: {}",
couponCode, throwable.getMessage());
return null; // 系统错误 → 优雅降级:不使用优惠继续结账
}
积分抵扣降级:
public boolean deductLoyaltyPointsForCheckoutFallback(...) {
if (throwable instanceof BusinessException businessException
&& shouldPropagateLoyaltyFailure(businessException)) {
throw businessException; // 余额不足 → 硬失败
}
log.warn("loyalty-service unavailable, skipping points deduction for buyer {}: {}",
buyerId, throwable.getMessage());
return false; // 系统错误 → 不使用积分继续结账
}
判断逻辑:业务逻辑错误(COUPON_INVALID、INSUFFICIENT_BALANCE、VALIDATION_ERROR)在当前实现里会直接中断流程;系统错误(DOWNSTREAM_ERROR、TIMEOUT)则触发优雅降级,跳过该步骤继续执行。
库存扣减不降级:
public void deductInventoryForCheckoutFallback(...) {
throw new BusinessException(CommonErrorCode.DOWNSTREAM_ERROR,
"Marketplace inventory is temporarily unavailable", throwable);
}
库存是核心依赖,如果库存服务不可用,结账直接失败——因为没有库存确认的订单无法发货。
游客购物流:无需注册也能下单#
Shop Platform 的买家策略是 Guest-First——用户不需要注册就能完成浏览、加购物车、结账的完整流程。
游客身份识别#
游客买家 ID 以 "guest-buyer-" 为前缀;这个身份来自 auth-server 签发的 guest JWT,而不是 BFF 自己生成的会话标识:
public boolean isGuestBuyer(String buyerId) {
return buyerId != null && buyerId.startsWith("guest-buyer-");
}
Redis 购物车#
游客购物车存储在 Redis 中,不使用数据库:
@Component
public class GuestCartStore {
private static final String CART_KEY_PREFIX = "buyer:guest:cart:";
private static final Duration DEFAULT_TTL = Duration.ofHours(48);
private final RedissonClient redissonClient;
public List<OrderApi.CartItemResponse> listCart(String buyerId) {
RBucket<String> bucket = redissonClient.getBucket(CART_KEY_PREFIX + buyerId);
String json = bucket.get();
if (json == null) return List.of();
return objectMapper.readValue(json, new TypeReference<>() {});
}
public void addToCart(String buyerId, OrderApi.CartItemRequest item) {
List<OrderApi.CartItemResponse> items = listCart(buyerId);
// 合并同类项逻辑
redissonClient.getBucket(CART_KEY_PREFIX + buyerId)
.set(objectMapper.writeValueAsString(items), DEFAULT_TTL.toMillis(), TimeUnit.MILLISECONDS);
}
}
BFF 中的购物车操作根据买家身份自动路由:
public OrderApi.CartView listCart(String buyerId) {
if (guestCartStore.isGuestBuyer(buyerId)) {
return guestCartStore.listCart(buyerId); // 走 Redis
}
return call("orderService", false,
() -> orderClient.listCart(buyerId), // 走 order-service DB
"Cart service unavailable");
}
游客结账#
游客结账走 order-service 的 GUEST_CHECKOUT 端点,创建的是带 order_token 的 GUEST 订单:
// GuestBuyerController
@PostMapping("/checkout")
public ApiResponse<OrderApi.OrderResponse> guestCheckout(
@Valid @RequestBody OrderApi.GuestCheckoutRequest request) {
return ApiResponse.success(service.guestCheckout(request));
}
// 游客订单追踪:通过 order_token 而非 buyerId
@GetMapping("/order/track")
public ApiResponse<OrderApi.OrderResponse> trackOrder(
@RequestParam String token) {
return ApiResponse.success(service.trackOrder(token));
}
游客凭 order_token 追踪订单,不需要登录。这保证了用户即使不注册也能知道自己的订单状态。
登录后购物车合并#
当游客注册/登录时,BFF 提供购物车合并端点:
@PostMapping("/cart/merge")
public ApiResponse<OrderApi.CartView> mergeCart(
@RequestHeader("X-Buyer-Id") String headerBuyerId,
@RequestHeader("X-Roles") String headerRoles,
@Valid @RequestBody BuyerApi.MergeGuestCartRequest request) {
requireSignedInBuyer(headerRoles, "Buyer cart merge");
return ApiResponse.success(service.mergeGuestCart(
requireAuthenticatedBuyerId(headerBuyerId, "Buyer cart merge"),
request.guestBuyerId()));
}
合并逻辑:
- 读取 Redis 中游客购物车的所有商品
- 逐个调用
order-service的addToCart加入登录用户的正式购物车 - 删除 Redis 中的游客购物车
这样游客在登录前已经加入的购物车内容通常可以继续保留。
DTO 组合与contract管理#
BFF 主要复用 shop-contracts 共享模块中的 DTO;自身只保留像 PageResponse 这样的聚合/分页包装对象:
// shop-contracts-buyer 中的 DashboardResponse
public record DashboardResponse(
ProfileApi.ProfileResponse profile, // 来自 profile-service
WalletApi.WalletAccountResponse wallet, // 来自 wallet-service
List<PromotionApi.OfferResponse> promotions, // 来自 promotion-service
List<MarketplaceApi.ProductResponse> marketplace, // 来自 marketplace-service
LoyaltyApi.AccountResponse loyalty) {} // 来自 loyalty-service
BFF 的职责是:
- 解包:从
ApiResponse<T>中提取data() - 组合:将多个下游 DTO 组合成前端需要的结构
- 包装:用
ApiResponse.success(T)返回
当搜索服务不可用时,BFF 还能将 marketplace 的商品列表映射为搜索响应格式,保证前端不会崩溃:
List<SearchApi.ProductHit> hits = fallback.products().stream()
.map(product -> new SearchApi.ProductHit(
product.id().toString(), product.sellerId(), product.name(),
product.description(), product.price(),
product.inventory() == null ? 0 : product.inventory(),
product.categoryId(), product.categoryName(),
product.imageUrl(), product.status()))
.toList();
测试策略#
BFF 的测试分为三层:
1. 单元测试(Mockito)#
BuyerAggregationServiceTest 验证优雅降级逻辑:
@Test
void validateCouponFallback_rethrowsDomainCouponErrors() {
// 业务错误(券无效)→ 在当前实现里继续抛出
when(service.validateCoupon(...))
.thenThrow(new BusinessException(CommonErrorCode.COUPON_INVALID, "..."));
assertThrows(BusinessException.class, () -> ...);
}
@Test
void validateCouponFallback_skipsOnSystemFailures() {
// 系统错误(服务不可用)→ 返回 null,继续结账
when(service.validateCoupon(...))
.thenThrow(new BusinessException(CommonErrorCode.DOWNSTREAM_ERROR, "..."));
var result = service.validateCouponForCheckout(...);
assertNull(result);
}
2. 控制器测试(@WebMvcTest)#
BuyerControllerTest 验证权限控制和指标采集:
@Test
void guestBuyerCannotMergeCart() {
mockMvc.perform(post("/buyer/v1/cart/merge")
.header("X-Roles", "ROLE_BUYER_GUEST"))
.andExpect(status().isForbidden());
}
@Test
void checkoutRecordsDurationMetrics() {
mockMvc.perform(post("/buyer/v1/checkout/create"))
.andExpect(status().isOk());
assertThat(registry.find("shop_order_checkout_duration_seconds")
.timer()).isNotNull();
}
3. WireMock contract testing(无 Spring 上下文)#
MarketplaceContractTest、OrderContractTest、LoyaltyContractTest 等使用 standalone WireMock 验证消费者侧的 JSON 反序列化和字段兼容性。这些测试不启动 Spring 上下文,不依赖 Redis,通过 mock ResilienceHelper 和 GuestCartStore 实现快速执行;它们更接近"消费者兼容性校验",而不是完整的 provider contract 平台。
参考与实现位置#
- Spring Framework REST Clients:https://docs.spring.io/spring-framework/reference/integration/rest-clients.html
- Spring Blog: New in Spring 6.1 RestClient:https://spring.io/blog/2023/07/13/new-in-spring-6-1-restclient
- Resilience4j Docs:https://resilience4j.readme.io/docs
- 仓库实现入口:
services/buyer-bff/src/main/java/dev/meirong/shop/buyerbff/service/BuyerAggregationService.java、services/buyer-bff/src/main/java/dev/meirong/shop/buyerbff/service/GuestCartStore.java、services/buyer-bff/src/main/java/dev/meirong/shop/buyerbff/config/BuyerBffConfig.java、services/seller-bff/src/main/java/dev/meirong/shop/sellerbff/config/SellerBffConfig.java、shared/shop-common/shop-common-core/src/main/java/dev/meirong/shop/common/resilience/ResilienceHelper.java、shared/shop-common/shop-common-core/src/main/java/dev/meirong/shop/common/http/HeaderPropagationAutoConfiguration.java
小结#
BFF 聚合层在这个项目里的主要实现特点:
- Virtual Threads 并发编排:
Executors.newVirtualThreadPerTaskExecutor()+Future.get(),同步代码享受异步并发 - Resilience4j 四层防护:CircuitBreaker + Retry + Bulkhead + TimeLimiter,每个下游服务独立配置
- Saga 结账补偿:逆向顺序补偿(库存→支付→积分),fire-and-forget 策略
- 优雅降级:优惠/积分不可用时跳过而非阻断,库存不可用时硬失败
- 游客购物车:Redis 存储、TTL 48 小时、登录后自动合并
- contract驱动:核心 DTO 尽量来自
shop-contracts,聚合层只补少量包装对象
下一篇将深入 领域服务设计:每服务独立数据库模式、Outbox Pattern 实现、JPA 组织方式、以及补偿任务的持久化重试机制。
项目仓库:github.com/meirongdev/shop(公开,可结合源码一起看)