Careeroza
Backend for Frontend
One Backend per Client Type
A web dashboard, an Android app and a smartwatch need very different data. One general-purpose API forces the mobile app to make many calls and download fields it never shows. A Backend for Frontend (BFF) is a thin service per client type, owned by that client's team, that composes and shapes data exactly for its screens.
React web app Android / iOS app Partner API
| | |
web-bff mobile-bff public-api gateway
(rich pages, admin) (small payloads, 1 call (stable, versioned,
per screen, image sizes) rate-limited)
\ | /
+---------- product, order, user, payment services -----+| web-bff | mobile-bff | |
|---|---|---|
| Home screen calls | 3 (page sections load independently) | 1 (GET /mobile/home) |
| Product image | 1200 px | 400 px, WebP |
| Fields per product | description, specs, reviews | name, price, rating, thumbnail |
| Auth | Session cookie (OAuth2 client, token relay) | Bearer token from the app |
A Mobile BFF Endpoint
@RestController
@RequestMapping("/mobile")
@RequiredArgsConstructor
public class MobileHomeController {
private final ProductClient products;
private final OrderClient orders;
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
// One round trip for the whole home screen: important on slow 3G/4G networks
@GetMapping("/home")
public MobileHome home(@AuthenticationPrincipal Jwt jwt) {
var deals = CompletableFuture.supplyAsync(() -> products.deals(10), executor);
var lastOrder = CompletableFuture.supplyAsync(() -> orders.latestFor(jwt.getSubject()), executor)
.exceptionally(ex -> null); // optional section
return new MobileHome(
deals.join().stream()
.map(p -> new MobileProductCard(p.id(), p.name(), p.price(), p.imageUrl() + "?w=400&fm=webp"))
.toList(),
lastOrder.join() == null ? null : new MobileOrderCard(lastOrder.join().id(), lastOrder.join().status()));
}
}
public record MobileProductCard(Long id, String name, BigDecimal price, String thumbnail) {}
public record MobileOrderCard(Long id, String status) {}
public record MobileHome(List<MobileProductCard> deals, MobileOrderCard lastOrder) {}Example
curl -s localhost:8090/mobile/home -H "Authorization: Bearer eyJhbGciOi..."Example output
{"deals":[{"id":1,"name":"boAt Rockerz 450","price":1499.00,"thumbnail":"https://cdn.shopease.in/p/1.jpg?w=400&fm=webp"},
{"id":4,"name":"Noise ColorFit Pro","price":3999.00,"thumbnail":"https://cdn.shopease.in/p/4.jpg?w=400&fm=webp"}],
"lastOrder":{"id":1001,"status":"SHIPPED"}}BFF Rules and Alternatives
- Keep business rules in the domain services. A BFF only composes, filters and formats.
- One BFF per experience, not per screen; owned by the frontend team.
- Avoid copy-paste between BFFs: shared logic belongs in a domain service.
- GraphQL (Spring for GraphQL with
@QueryMappingand@BatchMapping) is an alternative: one flexible endpoint where each client asks for exactly the fields it needs.