Patrón Backend for Frontend (BFF)
Dale a cada tipo de cliente su propio backend, que agrega los servicios de dominio y adapta la forma de los datos a cada plataforma.
Descripción General
El Patrón Backend for Frontend (BFF) crea servicios backend dedicados para cada tipo de cliente (web, móvil, IoT, escritorio). En lugar de forzar a todos los clientes a consumir una única API de propósito general, cada frontend obtiene un backend que agrega microservicios de dominio, adapta la forma de los datos, gestiona la autenticación y expone una API alineada con los requisitos de ese cliente.
Una app móvil puede necesitar respuestas ligeras y agregadas para ahorrar batería y ancho de banda. Un panel web puede necesitar datos muy anidados con actualizaciones en tiempo real. Un dispositivo IoT puede necesitar cargas binarias mínimas. Una única API no puede servir bien a los tres.
El BFF se sitúa entre el cliente y los microservicios de núcleo, como agregador, transformador y capa de caché específica de un tipo de cliente.
Cuándo Usar
Si dudas entre un BFF y un gateway GraphQL, consulta GraphQL vs REST — cuándo elegir y cómo migrar.
Usa el Patrón BFF cuando:
- Distintos clientes frontend tienen requisitos de datos incompatibles
- Los clientes móviles necesitan respuestas ligeras y menos llamadas de red
- Los clientes web necesitan renderizado en servidor o datos optimizados para SEO
- Las integraciones de terceros necesitan una superficie de API limitada y saneada
- Quieres iterar sobre las APIs del frontend sin afectar a otros clientes
Cuándo Evitar
- Solo tienes un tipo de cliente (una API gateway basta)
- El coste de mantener varios servicios BFF supera el beneficio
- Las necesidades de los frontends son casi idénticas entre plataformas
- El BFF se convierte en una capa gruesa con lógica de negocio que pertenece al dominio
Solución
Python
from dataclasses import dataclass, asdict
from typing import List
import json
# ============================================================================
# CORE MICROSERVICES (servicios backend compartidos)
# ============================================================================
class UserService:
def get_user(self, user_id: str) -> dict:
return {
"id": user_id,
"name": "Alice Johnson",
"email": "alice@example.com",
"address": {"street": "123 Main St", "city": "NYC", "zip": "10001"},
"preferences": {"theme": "dark", "notifications": True},
"created_at": "2022-01-15T00:00:00Z"
}
class OrderService:
def get_orders(self, user_id: str) -> List[dict]:
return [
{"id": "ORD-001", "user_id": user_id, "total": 99.99,
"items": [{"sku": "A1", "name": "Widget", "price": 99.99}],
"status": "delivered", "created_at": "2024-03-01"},
{"id": "ORD-002", "user_id": user_id, "total": 45.00,
"items": [{"sku": "B2", "name": "Gadget", "price": 45.00}],
"status": "shipped", "created_at": "2024-03-10"}
]
class InventoryService:
def get_stock(self, sku: str) -> dict:
return {"sku": sku, "quantity": 150, "warehouse": "US-East"}
# ============================================================================
# BFF PARA CLIENTE MOBILE
# ============================================================================
@dataclass
class MobileOrderSummary:
order_id: str
total: float
item_count: int
status: str
@dataclass
class MobileUserProfile:
name: str
recent_orders: List[MobileOrderSummary]
unread_notifications: int
class MobileBFF:
"""BFF optimizado para mobile: ligero, agregado, mínimos campos"""
def __init__(self, user_service: UserService, order_service: OrderService):
self.users = user_service
self.orders = order_service
def get_profile(self, user_id: str) -> MobileUserProfile:
user = self.users.get_user(user_id)
orders = self.orders.get_orders(user_id)
recent_orders = [
MobileOrderSummary(
order_id=o["id"],
total=o["total"],
item_count=len(o["items"]),
status=o["status"]
)
for o in orders[:3] # Solo últimas 3 órdenes para mobile
]
return MobileUserProfile(
name=user["name"],
recent_orders=recent_orders,
unread_notifications=2 # Agregado desde notification service
)
# ============================================================================
# BFF PARA CLIENTE WEB
# ============================================================================
class WebBFF:
"""BFF optimizado para web: datos ricos, relaciones anidadas, historial completo"""
def __init__(self, user_service: UserService, order_service: OrderService,
inventory_service: InventoryService):
self.users = user_service
self.orders = order_service
self.inventory = inventory_service
def get_profile(self, user_id: str) -> dict:
user = self.users.get_user(user_id)
orders = self.orders.get_orders(user_id)
# Enriquecer órdenes con datos de inventario
enriched_orders = []
for order in orders:
enriched_items = []
for item in order["items"]:
stock = self.inventory.get_stock(item["sku"])
enriched_items.append({
**item,
"in_stock": stock["quantity"] > 0,
"warehouse": stock["warehouse"]
})
enriched_orders.append({**order, "items": enriched_items})
return {
"user": {
"id": user["id"],
"name": user["name"],
"email": user["email"],
"member_since": user["created_at"]
},
"orders": enriched_orders,
"order_count": len(orders),
"total_spent": sum(o["total"] for o in orders)
}
# ============================================================================
# USO
# ============================================================================
users = UserService()
orders = OrderService()
inventory = InventoryService()
mobile_bff = MobileBFF(users, orders)
web_bff = WebBFF(users, orders, inventory)
mobile_profile = mobile_bff.get_profile("U-123")
print("MOBILE:", json.dumps(asdict(mobile_profile), indent=2))
web_profile = web_bff.get_profile("U-123")
print("WEB:", json.dumps(web_profile, indent=2))
Java
import java.util.*;
import java.util.stream.*;
// Core services
class UserService {
public Map<String, Object> getUser(String userId) {
Map<String, Object> user = new HashMap<>();
user.put("id", userId);
user.put("name", "Alice Johnson");
user.put("email", "alice@example.com");
user.put("createdAt", "2022-01-15T00:00:00Z");
return user;
}
}
class OrderService {
public List<Map<String, Object>> getOrders(String userId) {
List<Map<String, Object>> orders = new ArrayList<>();
Map<String, Object> o1 = new HashMap<>();
o1.put("id", "ORD-001"); o1.put("total", 99.99); o1.put("status", "delivered");
orders.add(o1);
return orders;
}
}
// Mobile BFF
class MobileBFF {
private final UserService users;
private final OrderService orders;
public MobileBFF(UserService users, OrderService orders) {
this.users = users; this.orders = orders;
}
public Map<String, Object> getProfile(String userId) {
Map<String, Object> user = users.getUser(userId);
List<Map<String, Object>> allOrders = orders.getOrders(userId);
List<Map<String, Object>> recent = allOrders.stream()
.limit(3)
.map(o -> {
Map<String, Object> m = new HashMap<>();
m.put("orderId", o.get("id"));
m.put("total", o.get("total"));
m.put("status", o.get("status"));
return m;
})
.collect(Collectors.toList());
Map<String, Object> profile = new HashMap<>();
profile.put("name", user.get("name"));
profile.put("recentOrders", recent);
return profile;
}
}
// Web BFF
class WebBFF {
private final UserService users;
private final OrderService orders;
public WebBFF(UserService users, OrderService orders) {
this.users = users; this.orders = orders;
}
public Map<String, Object> getProfile(String userId) {
Map<String, Object> user = users.getUser(userId);
List<Map<String, Object>> allOrders = orders.getOrders(userId);
Map<String, Object> profile = new HashMap<>();
profile.put("user", user);
profile.put("orders", allOrders);
profile.put("orderCount", allOrders.size());
double total = allOrders.stream()
.mapToDouble(o -> ((Number) o.get("total")).doubleValue())
.sum();
profile.put("totalSpent", total);
return profile;
}
}
// Uso
public class BFFDemo {
public static void main(String[] args) {
UserService users = new UserService();
OrderService orders = new OrderService();
MobileBFF mobile = new MobileBFF(users, orders);
WebBFF web = new WebBFF(users, orders);
System.out.println(mobile.getProfile("U-123"));
System.out.println(web.getProfile("U-123"));
}
}
JavaScript
// Core microservices
class UserService {
async getUser(userId) {
return { id: userId, name: 'Alice Johnson', email: 'alice@example.com', createdAt: '2022-01-15' };
}
}
class OrderService {
async getOrders(userId) {
return [
{ id: 'ORD-001', total: 99.99, status: 'delivered', items: [{ sku: 'A1', name: 'Widget' }] },
{ id: 'ORD-002', total: 45.00, status: 'shipped', items: [{ sku: 'B2', name: 'Gadget' }] }
];
}
}
class InventoryService {
async getStock(sku) {
return { sku, quantity: 150, warehouse: 'US-East' };
}
}
// Mobile BFF
class MobileBFF {
constructor(userService, orderService) {
this.users = userService;
this.orders = orderService;
}
async getProfile(userId) {
const [user, allOrders] = await Promise.all([
this.users.getUser(userId),
this.orders.getOrders(userId)
]);
const recentOrders = allOrders.slice(0, 3).map(o => ({
orderId: o.id,
total: o.total,
status: o.status,
itemCount: o.items.length
}));
return {
name: user.name,
recentOrders,
unreadNotifications: 2
};
}
}
// Web BFF
class WebBFF {
constructor(userService, orderService, inventoryService) {
this.users = userService;
this.orders = orderService;
this.inventory = inventoryService;
}
async getProfile(userId) {
const [user, allOrders] = await Promise.all([
this.users.getUser(userId),
this.orders.getOrders(userId)
]);
const enrichedOrders = await Promise.all(
allOrders.map(async order => {
const enrichedItems = await Promise.all(
order.items.map(async item => {
const stock = await this.inventory.getStock(item.sku);
return { ...item, inStock: stock.quantity > 0, warehouse: stock.warehouse };
})
);
return { ...order, items: enrichedItems };
})
);
return {
user,
orders: enrichedOrders,
orderCount: allOrders.length,
totalSpent: allOrders.reduce((sum, o) => sum + o.total, 0)
};
}
}
// Uso
async function demo() {
const users = new UserService();
const orders = new OrderService();
const inventory = new InventoryService();
const mobile = new MobileBFF(users, orders);
const web = new WebBFF(users, orders, inventory);
console.log('Mobile:', await mobile.getProfile('U-123'));
console.log('Web:', await web.getProfile('U-123'));
}
demo().catch(console.error);
Explicación
El BFF es una fachada de API por tipo de cliente:
- Agregación: el BFF llama a varios servicios de dominio y ensambla una respuesta unificada
- Transformación: los datos se remodelan según lo que el cliente necesita (móvil recibe resúmenes, web recibe objetos completos)
- Optimización: tamaño de respuesta, caché y selección de campos se ajustan por plataforma
- Aislamiento: los cambios en la UI web no afectan a los clientes móviles, y viceversa
Cada BFF pertenece al equipo responsable de ese frontend, lo que les permite iterar de forma independiente. A diferencia de un punto de entrada compartido como el Patrón Gateway Routing o una Fachada de propósito general, un BFF responde a una única experiencia de cliente — Sam Newman documentó el patrón en Backends For Frontends.
Variantes
| Variante | Tipo de Cliente | Especialización |
|---|---|---|
| BFF móvil | iOS/Android | Respuestas pequeñas, resiliencia de conexión, soporte offline |
| BFF web | Navegador/SPA | SSR, SEO, anidamiento profundo de datos, tiempo real |
| BFF IoT | Dispositivos embebidos | Protocolos binarios, JSON mínimo, caché en el borde |
| BFF para partners | Integraciones de terceros | Limitación de tasa, datos restringidos, versionado de API |
| BFF GraphQL | Cualquiera | Schema stitching, agregación basada en resolvers |
BFF con GraphQL
Cuando los clientes difieren sobre todo en la selección de campos, un único BFF GraphQL puede servir a todos:
const { ApolloServer } = require("@apollo/server");
const { startStandaloneServer } = require("@apollo/server/standalone");
const axios = require("axios");
const typeDefs = `#graphql
type Dashboard { user: User!, recentOrders: [Order!]! }
type User { id: ID!, name: String!, avatarUrl: String }
type Order { id: ID!, status: String!, total: Float! }
type Query { dashboard(userId: ID!): Dashboard! }
`;
const resolvers = {
Query: {
async dashboard(_, { userId }) {
const [user, orders] = await Promise.all([
axios.get(`http://user-service:3001/users/${userId}`),
axios.get(`http://order-service:3002/users/${userId}/orders?limit=5`)
]);
return { user: user.data, recentOrders: orders.data.orders };
}
}
};
startStandaloneServer(new ApolloServer({ typeDefs, resolvers }), { listen: { port: 4000 } });
BFF con caché
Las agregaciones tipo dashboard cambian despacio — cachea en el BFF por 30–60 s:
const redis = require("redis");
const express = require("express");
const axios = require("axios");
const app = express();
const cache = redis.createClient({ url: process.env.REDIS_URL });
await cache.connect();
app.get("/api/dashboard", async (req, res) => {
const key = `dashboard:${req.headers["x-user-id"]}`;
const cached = await cache.get(key).catch(() => null);
if (cached) return res.json(JSON.parse(cached));
const [user, orders] = await Promise.all([
axios.get(`http://user-service:3001/users/${req.headers["x-user-id"]}`),
axios.get(`http://order-service:3002/users/${req.headers["x-user-id"]}/orders?limit=5`)
]);
const body = { user: user.data, recentOrders: orders.data.orders };
await cache.setEx(key, 60, JSON.stringify(body)).catch(() => {});
res.json(body);
});
Otras variantes útiles: un BFF con WebSocket empuja actualizaciones en vivo (estado de pedidos, inventario) a los clientes suscritos en lugar de polling, y un BFF compartido con response shapers por tipo de cliente funciona cuando un solo deployable basta pero las cargas divergen. Para el control de flujo push-pull dentro del propio BFF, ver el Patrón Back-Pressure.
Lo que funciona
- Mantén los BFF delgados. Deben agregar y transformar, no implementar lógica de negocio del dominio.
- Ubica el BFF junto al equipo de frontend. El equipo que posee el cliente debería poseer su BFF.
- Usa GraphQL cuando los clientes necesitan consultas flexibles. Un único BFF GraphQL puede reemplazar varios BFF REST.
- Cachea agresivamente en el BFF. Reduce llamadas a servicios de dominio con caché basada en TTL.
- Comparte código vía librerías. Clientes HTTP comunes, middleware de autenticación y logging deberían ser código de librería compartida.
Errores Comunes
- Duplicar lógica de negocio. El BFF no debería reimplementar reglas de validación o cálculo del dominio.
- Sobre-pedir a los servicios de dominio. Usa llamadas específicas, no “traerlo todo y filtrar”.
- Un BFF para todos los clientes. Eso anula el patrón y recrea el mismo acoplamiento de una API general.
- No manejar fallos con elegancia. Que un servicio de dominio falle no debería romper toda la respuesta del BFF.
- Ignorar el contexto de autenticación. Los BFF deberían propagar identidad y permisos del usuario a los servicios de dominio.
Ejemplos del Mundo Real
Netflix
Netflix opera un servicio de borde dedicado por experiencia de cliente: su equipo de Android reemplazó el backend de la app por un endpoint a medida que controlan por completo — documentado en el artículo de AWS sobre el patrón BFF.
SoundCloud
El patrón se acuñó en SoundCloud: Phil Calçado lo describió en The Back-end for Front-end Pattern, y la empresa sigue operando decenas de BFF — Mobile API, Web API y APIs pública y de partners — que procesan cientos de millones de peticiones por hora, según su blog de ingeniería Backstage.
Origen del patrón
Sam Newman formalizó BFF tras observarlo en REA y SoundCloud, y su artículo del patrón sigue siendo la referencia: un backend de servidor por experiencia de usuario, propiedad del equipo que publica el cliente.
Preguntas frecuentes
¿Cuál es la diferencia entre BFF y API Gateway?
Un API Gateway es un punto de entrada compartido y genérico. Un BFF es un backend específico de un cliente. Puedes tener un API Gateway delante de varios BFF.
¿Puede un BFF servir a varios clientes similares?
Sí, si sus necesidades son casi idénticas (p. ej. iOS y Android). Pero resiste la tentación de generalizar: el valor del BFF está en la especificidad.
¿Los BFF deberían manejar autenticación?
Normalmente validan tokens y propagan la identidad, pero el proveedor de identidad real (OAuth, OIDC) debería ser un servicio compartido.
¿Cómo se relacionan los BFF con los microservicios?
Se sitúan por encima de los microservicios: son consumidores de esa capa, no un reemplazo de ella.
¿iOS y Android deberían tener BFF separados?
Solo si sus necesidades divergen. SoundCloud sirve ambas apps desde un único BFF móvil; REA usaba uno por plataforma. Comparte mientras pidan los mismos datos — separa cuando dejen de hacerlo.
¿Dónde vive la lógica compartida?
En los servicios de dominio o en librerías compartidas. El BFF se mantiene delgado: agregación, transformación, caché y preocupaciones de borde como propagación de auth y limitación de tasa — nunca reglas de negocio reimplementadas.
¿GraphQL reemplaza al BFF?
Un gateway GraphQL puede actuar como un único BFF cuando los clientes difieren sobre todo en la selección de campos. Si necesitan caché, auth o cadencias de release distintas, BFF separados siguen ayudando — y puedes construir cada uno sobre GraphQL.
Recursos Relacionados
Patrón Facade
Provee una interfaz simplificada a un subsistema complejo. Un patrón estructural que oculta detalles de implementación detrás de una API limpia.
PatternAbstract Factory para Familias de Componentes UI
Crea familias de objetos relacionados sin especificar clases concretas, permitiendo implementaciones específicas de plataforma que comparten una interfaz común.
PatternGraphQL Mutation Validation: Validación Centralizada
Centralizá la validación de mutaciones GraphQL con reglas reutilizables, scalars y errores estructurados. Ejemplos en TypeScript y mejores prácticas.
PatternPatron de Enrutamiento de Gateway
Enruta solicitudes a multiples servicios backend a traves de un unico punto de entrada que gestiona preocupaciones transversales.
PatternPatrón Mediator para Componentes UI Desacoplados
Reduce dependencias caóticas entre componentes UI introduciendo un mediador que centraliza la comunicación y evita referencias explícitas entre pares
PatternPatrón de Hosting de Contenido Estático
Despliega archivos estáticos en una red de entrega de contenido o almacenamiento de objetos para descargar servidores de origen, reducir latencia y mejorar disponibilidad.