Client-Specific
BFF provides tailored APIs for each client type. Mobile gets minimal data, web gets full features.
Traditional approach: One API for all clients.
Problems:
Solution: Backend for Frontend (BFF) - Each client type gets its own tailored backend API, optimized for its specific needs and constraints.
BFF (Backend for Frontend) is an architectural pattern where each client type (mobile, web, desktop) gets its own tailored backend API optimized for its specific needs. Instead of forcing all clients to use a one-size-fits-all API, BFF creates specialized backends that understand each client’s requirements.
The BFF pattern recognizes that different clients have fundamentally different needs:
By creating a dedicated backend for each frontend, you can optimize data transfer, reduce over-fetching, and provide client-specific features without compromising any client’s experience.
Each BFF:
Mobile needs:
{ "id": 123, "name": "John", "avatar": "small_url"}Size: ~100 bytes
Web needs:
{ "id": 123, "name": "John Doe", "avatar": "large_url", "bio": "Long bio text...", "preferences": {...}, "recentActivity": [...], "friends": [...]}Size: ~5000 bytes
Without BFF: Mobile gets 5000 bytes (wastes 4900 bytes!)
With BFF: Mobile gets 100 bytes (50x smaller!)
Each client has dedicated BFF:
Pros:
Cons:
One BFF with client-specific adapters:
Pros:
Cons:
from flask import Flask, jsonifyimport requests
app = Flask(__name__)
SERVICES = { 'users': 'http://user-service:8001', 'orders': 'http://order-service:8002',}
@app.route('/api/mobile/users/<user_id>')def get_mobile_user(user_id): """Mobile-optimized user endpoint - minimal data""" # Fetch from user service user_response = requests.get(f"{SERVICES['users']}/users/{user_id}") user = user_response.json()
# Transform for mobile (minimal data) mobile_user = { 'id': user['id'], 'name': user['name'], 'avatar': user.get('avatar_small'), # Small avatar for mobile }
return jsonify(mobile_user)
@app.route('/api/mobile/users/<user_id>/orders')def get_mobile_orders(user_id): """Mobile-optimized orders - summary only""" orders_response = requests.get( f"{SERVICES['orders']}/orders?userId={user_id}" ) orders = orders_response.json()
# Transform for mobile (summary only) mobile_orders = [ { 'id': order['id'], 'total': order['total'], 'status': order['status'], 'date': order['created_at'][:10] # Just date, not full timestamp } for order in orders ]
return jsonify(mobile_orders)import org.springframework.web.bind.annotation.*;import org.springframework.web.client.RestTemplate;
@RestController@RequestMapping("/api/mobile")public class MobileBFF { private final RestTemplate restTemplate; private final String userServiceUrl = "http://user-service:8001"; private final String orderServiceUrl = "http://order-service:8002";
@GetMapping("/users/{userId}") public MobileUserDTO getMobileUser(@PathVariable String userId) { // Fetch from user service User user = restTemplate.getForObject( userServiceUrl + "/users/" + userId, User.class );
// Transform for mobile (minimal data) return MobileUserDTO.builder() .id(user.getId()) .name(user.getName()) .avatar(user.getAvatarSmall()) // Small avatar for mobile .build(); }
@GetMapping("/users/{userId}/orders") public List<MobileOrderDTO> getMobileOrders(@PathVariable String userId) { // Fetch from order service List<Order> orders = restTemplate.getForObject( orderServiceUrl + "/orders?userId=" + userId, List.class );
// Transform for mobile (summary only) return orders.stream() .map(order -> MobileOrderDTO.builder() .id(order.getId()) .total(order.getTotal()) .status(order.getStatus()) .date(order.getCreatedAt().toString().substring(0, 10)) .build()) .collect(Collectors.toList()); }}import express from 'express';import axios from 'axios';
const app = express();
const SERVICES = { users: 'http://user-service:8001', orders: 'http://order-service:8002',};
app.get('/api/mobile/users/:userId', async (req, res) => { // Mobile-optimized user endpoint - minimal data const userId = req.params.userId;
// Fetch from user service const userResponse = await axios.get(`${SERVICES.users}/users/${userId}`); const user = userResponse.data;
// Transform for mobile (minimal data) const mobileUser = { id: user.id, name: user.name, avatar: user.avatar_small, // Small avatar for mobile };
res.json(mobileUser);});
app.get('/api/mobile/users/:userId/orders', async (req, res) => { // Mobile-optimized orders - summary only const userId = req.params.userId;
const ordersResponse = await axios.get( `${SERVICES.orders}/orders?userId=${userId}` ); const orders = ordersResponse.data;
// Transform for mobile (summary only) const mobileOrders = orders.map((order: any) => ({ id: order.id, total: order.total, status: order.status, date: order.created_at.substring(0, 10), // Just date, not full timestamp }));
res.json(mobileOrders);});#include <cpprest/http_listener.h>#include <cpprest/http_client.h>#include <cpprest/json.h>
class MobileBFF {private: web::http::client::http_client userClient; web::http::client::http_client orderClient;
public: MobileBFF() : userClient("http://user-service:8001"), orderClient("http://order-service:8002") {}
void handleGetMobileUser(web::http::http_request request) { auto userId = extractUserId(request);
// Fetch from user service auto userRequest = userClient.request( web::http::methods::GET, "/users/" + userId );
userRequest.then([=](web::http::http_response response) { return response.extract_json(); }).then([=](json::value user) { // Transform for mobile (minimal data) json::value mobileUser; mobileUser["id"] = user["id"]; mobileUser["name"] = user["name"]; mobileUser["avatar"] = user["avatar_small"];
request.reply(web::http::status_codes::OK, mobileUser); }); }};using Microsoft.AspNetCore.Mvc;using System.Net.Http;using System.Threading.Tasks;
[ApiController][Route("api/mobile")]public class MobileBFFController : ControllerBase { private readonly HttpClient httpClient;
[HttpGet("users/{userId}")] public async Task<IActionResult> GetMobileUser(string userId) { // Mobile-optimized user endpoint - minimal data var userResponse = await httpClient.GetAsync( $"http://user-service:8001/users/{userId}" ); var user = await userResponse.Content.ReadFromJsonAsync<User>();
// Transform for mobile (minimal data) var mobileUser = new { id = user.Id, name = user.Name, avatar = user.AvatarSmall // Small avatar for mobile };
return Ok(mobileUser); }
[HttpGet("users/{userId}/orders")] public async Task<IActionResult> GetMobileOrders(string userId) { // Mobile-optimized orders - summary only var ordersResponse = await httpClient.GetAsync( $"http://order-service:8002/orders?userId={userId}" ); var orders = await ordersResponse.Content.ReadFromJsonAsync<List<Order>>();
// Transform for mobile (summary only) var mobileOrders = orders.Select(o => new { id = o.Id, total = o.Total, status = o.Status, date = o.CreatedAt.ToString("yyyy-MM-dd") // Just date });
return Ok(mobileOrders); }}from flask import Flask, jsonifyimport requests
app = Flask(__name__)
SERVICES = { 'users': 'http://user-service:8001', 'orders': 'http://order-service:8002', 'products': 'http://product-service:8003',}
@app.route('/api/web/users/<user_id>')def get_web_user(user_id): """Web-optimized user endpoint - full data""" # Fetch from multiple services import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor: user_future = executor.submit( requests.get, f"{SERVICES['users']}/users/{user_id}" ) orders_future = executor.submit( requests.get, f"{SERVICES['orders']}/orders?userId={user_id}" ) preferences_future = executor.submit( requests.get, f"{SERVICES['users']}/users/{user_id}/preferences" )
user = user_future.result().json() orders = orders_future.result().json() preferences = preferences_future.result().json()
# Transform for web (full data) web_user = { 'id': user['id'], 'name': user['name'], 'email': user['email'], 'avatar': user.get('avatar_large'), # Large avatar for web 'bio': user.get('bio'), 'preferences': preferences, 'recentOrders': orders[:5], # Recent orders 'stats': { 'totalOrders': len(orders), 'totalSpent': sum(o['total'] for o in orders), } }
return jsonify(web_user)import org.springframework.web.bind.annotation.*;import org.springframework.web.client.RestTemplate;import reactor.core.publisher.Mono;
@RestController@RequestMapping("/api/web")public class WebBFF { private final WebClient webClient;
@GetMapping("/users/{userId}") public Mono<WebUserDTO> getWebUser(@PathVariable String userId) { // Fetch from multiple services in parallel Mono<User> user = webClient.get() .uri("http://user-service:8001/users/{userId}", userId) .retrieve() .bodyToMono(User.class);
Mono<List<Order>> orders = webClient.get() .uri("http://order-service:8002/orders?userId={userId}", userId) .retrieve() .bodyToFlux(Order.class) .collectList();
Mono<Preferences> preferences = webClient.get() .uri("http://user-service:8001/users/{userId}/preferences", userId) .retrieve() .bodyToMono(Preferences.class);
// Combine and transform for web return Mono.zip(user, orders, preferences) .map(tuple -> { User u = tuple.getT1(); List<Order> o = tuple.getT2(); Preferences p = tuple.getT3();
return WebUserDTO.builder() .id(u.getId()) .name(u.getName()) .email(u.getEmail()) .avatar(u.getAvatarLarge()) // Large avatar for web .bio(u.getBio()) .preferences(p) .recentOrders(o.stream().limit(5).collect(Collectors.toList())) .stats(Stats.builder() .totalOrders(o.size()) .totalSpent(o.stream().mapToDouble(Order::getTotal).sum()) .build()) .build(); }); }}Adapters transform data for each client:
from abc import ABC, abstractmethodfrom typing import Dict, Any
class UserAdapter(ABC): """Base adapter interface"""
@abstractmethod def transform(self, user: Dict[str, Any]) -> Dict[str, Any]: """Transform user data for specific client""" pass
class MobileUserAdapter(UserAdapter): """Adapter for mobile - minimal data"""
def transform(self, user: Dict[str, Any]) -> Dict[str, Any]: return { 'id': user['id'], 'name': user['name'], 'avatar': user.get('avatar_small'), }
class WebUserAdapter(UserAdapter): """Adapter for web - full data"""
def transform(self, user: Dict[str, Any]) -> Dict[str, Any]: return { 'id': user['id'], 'name': user['name'], 'email': user['email'], 'avatar': user.get('avatar_large'), 'bio': user.get('bio'), 'preferences': user.get('preferences', {}), 'stats': user.get('stats', {}), }
class BFFService: """BFF service using adapters"""
def __init__(self, adapter: UserAdapter): self.adapter = adapter
def get_user(self, user_id: str) -> Dict[str, Any]: # Fetch from backend service user = user_service.get_user(user_id)
# Transform using adapter return self.adapter.transform(user)interface UserAdapter { UserDTO transform(User user);}
class MobileUserAdapter implements UserAdapter { @Override public UserDTO transform(User user) { return MobileUserDTO.builder() .id(user.getId()) .name(user.getName()) .avatar(user.getAvatarSmall()) .build(); }}
class WebUserAdapter implements UserAdapter { @Override public UserDTO transform(User user) { return WebUserDTO.builder() .id(user.getId()) .name(user.getName()) .email(user.getEmail()) .avatar(user.getAvatarLarge()) .bio(user.getBio()) .preferences(user.getPreferences()) .stats(user.getStats()) .build(); }}
class BFFService { private final UserAdapter adapter; private final UserService userService;
public BFFService(UserAdapter adapter, UserService userService) { this.adapter = adapter; this.userService = userService; }
public UserDTO getUser(String userId) { User user = userService.getUser(userId); return adapter.transform(user); }}interface UserAdapter { transform(user: User): UserDTO;}
class MobileUserAdapter implements UserAdapter { transform(user: User): MobileUserDTO { // Adapter for mobile - minimal data return { id: user.id, name: user.name, avatar: user.avatarSmall, }; }}
class WebUserAdapter implements UserAdapter { transform(user: User): WebUserDTO { // Adapter for web - full data return { id: user.id, name: user.name, email: user.email, avatar: user.avatarLarge, bio: user.bio, preferences: user.preferences, stats: user.stats, }; }}
class BFFService { constructor(private adapter: UserAdapter, private userService: UserService) {}
getUser(userId: string): UserDTO { const user = this.userService.getUser(userId); return this.adapter.transform(user); }}class UserAdapter {public: virtual ~UserAdapter() = default; virtual json::value transform(const json::value& user) = 0;};
class MobileUserAdapter : public UserAdapter {public: json::value transform(const json::value& user) override { // Adapter for mobile - minimal data json::value mobileUser; mobileUser["id"] = user["id"]; mobileUser["name"] = user["name"]; mobileUser["avatar"] = user["avatar_small"]; return mobileUser; }};
class WebUserAdapter : public UserAdapter {public: json::value transform(const json::value& user) override { // Adapter for web - full data json::value webUser; webUser["id"] = user["id"]; webUser["name"] = user["name"]; webUser["email"] = user["email"]; webUser["avatar"] = user["avatar_large"]; webUser["bio"] = user["bio"]; webUser["preferences"] = user["preferences"]; return webUser; }};
class BFFService {private: std::unique_ptr<UserAdapter> adapter; UserService& userService;
public: BFFService(std::unique_ptr<UserAdapter> adapter, UserService& userService) : adapter(std::move(adapter)), userService(userService) {}
json::value getUser(const std::string& userId) { auto user = userService.getUser(userId); return adapter->transform(user); }};public interface IUserAdapter { UserDTO Transform(User user);}
public class MobileUserAdapter : IUserAdapter { public UserDTO Transform(User user) { // Adapter for mobile - minimal data return new MobileUserDTO { Id = user.Id, Name = user.Name, Avatar = user.AvatarSmall }; }}
public class WebUserAdapter : IUserAdapter { public UserDTO Transform(User user) { // Adapter for web - full data return new WebUserDTO { Id = user.Id, Name = user.Name, Email = user.Email, Avatar = user.AvatarLarge, Bio = user.Bio, Preferences = user.Preferences, Stats = user.Stats }; }}
public class BFFService { private readonly IUserAdapter adapter; private readonly UserService userService;
public BFFService(IUserAdapter adapter, UserService userService) { this.adapter = adapter; this.userService = userService; }
public UserDTO GetUser(string userId) { var user = userService.GetUser(userId); return adapter.Transform(user); }}| Feature | API Gateway | BFF |
|---|---|---|
| Purpose | Routing, cross-cutting concerns | Client-specific optimization |
| Aggregation | Can aggregate | Always aggregates |
| Transformation | Minimal | Heavy transformation |
| Client Awareness | Generic | Client-specific |
| Use Case | Single entry point | Client optimization |
They can work together:
Client → API Gateway → BFF → ServicesClient-Specific
BFF provides tailored APIs for each client type. Mobile gets minimal data, web gets full features.
Performance Optimized
Each BFF optimizes for its client. Mobile saves bandwidth, web gets rich data.
Adapter Pattern
Use adapter pattern to transform data for each client. Shared logic, client-specific transformations.
Code Reuse
Share common logic in libraries. BFFs can share code while providing client-specific APIs.