# Backend for Frontend (BFF)

## Problem

Different client types (mobile app, web SPA, smart TV, CLI) have different data needs, bandwidth constraints, and interaction patterns. A single generic API either over-fetches data (wasting mobile bandwidth) or under-fetches it (requiring multiple round-trips from the web app). Trying to serve all clients from one API leads to a bloated, compromised design that serves no client well.

## Solution

Create a **Backend for Frontend** — a dedicated backend service (or layer) for each client type. Each BFF:

- **Aggregates** calls to multiple microservices into a single response tailored to its client.
- **Transforms** data into the shape the client needs (field selection, pagination, formatting).
- **Optimises** payload size, number of round-trips, and caching strategy per client.
- Is **owned by the client team**, so frontend and backend evolve together.

## When to Use

- Multiple client types with significantly different data needs.
- Mobile clients need minimal payloads; web clients need rich data.
- Client teams want to evolve their API independently.
- You're aggregating data from multiple backend microservices.

## When to Avoid

- A single client type — one API is sufficient.
- Clients have nearly identical data needs — a shared API with field selection (GraphQL) may suffice.
- The BFF would just be a pass-through with no aggregation or transformation.

## Pseudocode

```
class MobileBFF:
    function get_dashboard(user_id):
        user = user_service.get_summary(user_id)   // minimal fields
        orders = order_service.get_recent(user_id, limit=3)
        return {
            name: user.name,
            recent_orders: orders.count,
            last_order_status: orders[0].status,
        }

class WebBFF:
    function get_dashboard(user_id):
        user = user_service.get_full(user_id)       // all fields
        orders = order_service.get_all(user_id)
        recommendations = rec_service.get(user_id)
        return {
            user: user,
            orders: orders,
            recommendations: recommendations,
        }
```

## Python

```python
"""Backend for Frontend (BFF) pattern in Python."""
from dataclasses import dataclass, field


# ── Simulated backend microservices ───────────────────────────

class UserService:
    _users = {
        "U1": {"id": "U1", "name": "Alice", "email": "alice@example.com",
               "avatar": "https://img.example.com/alice.png",
               "bio": "Software engineer", "joined": "2023-01-15"},
    }

    def get_full(self, user_id: str) -> dict | None:
        return self._users.get(user_id)

    def get_summary(self, user_id: str) -> dict | None:
        user = self._users.get(user_id)
        if user:
            return {"id": user["id"], "name": user["name"]}
        return None


class OrderService:
    _orders = {
        "U1": [
            {"id": "O1", "item": "Widget", "total": 29.99, "status": "shipped", "date": "2024-03-01"},
            {"id": "O2", "item": "Gadget", "total": 49.99, "status": "delivered", "date": "2024-02-15"},
            {"id": "O3", "item": "Doohickey", "total": 14.99, "status": "processing", "date": "2024-03-10"},
        ],
    }

    def get_all(self, user_id: str) -> list[dict]:
        return self._orders.get(user_id, [])

    def get_recent(self, user_id: str, limit: int = 3) -> list[dict]:
        orders = self._orders.get(user_id, [])
        return orders[:limit]


class RecommendationService:
    def get(self, user_id: str) -> list[dict]:
        return [
            {"id": "P1", "name": "Thingamajig", "price": 19.99},
            {"id": "P2", "name": "Whatchamacallit", "price": 34.99},
        ]


# ── BFF for Mobile ───────────────────────────────────────────

class MobileBFF:
    """Optimised for mobile: minimal payload, few fields, single call."""

    def __init__(self, user_svc: UserService, order_svc: OrderService):
        self.user_svc = user_svc
        self.order_svc = order_svc

    def get_dashboard(self, user_id: str) -> dict:
        user = self.user_svc.get_summary(user_id)
        if not user:
            return {"error": "user not found"}

        recent = self.order_svc.get_recent(user_id, limit=2)
        return {
            "name": user["name"],
            "recent_order_count": len(recent),
            "last_order_status": recent[0]["status"] if recent else None,
        }


# ── BFF for Web ──────────────────────────────────────────────

class WebBFF:
    """Optimised for web: rich data, multiple sections, single call."""

    def __init__(self, user_svc: UserService, order_svc: OrderService,
                 rec_svc: RecommendationService):
        self.user_svc = user_svc
        self.order_svc = order_svc
        self.rec_svc = rec_svc

    def get_dashboard(self, user_id: str) -> dict:
        user = self.user_svc.get_full(user_id)
        if not user:
            return {"error": "user not found"}

        orders = self.order_svc.get_all(user_id)
        recommendations = self.rec_svc.get(user_id)

        return {
            "user": user,
            "orders": orders,
            "order_total": sum(o["total"] for o in orders),
            "recommendations": recommendations,
        }


# ── Tests ─────────────────────────────────────────────────────

def test_mobile_bff_minimal_payload():
    bff = MobileBFF(UserService(), OrderService())
    result = bff.get_dashboard("U1")

    assert "name" in result
    assert "email" not in result  # mobile doesn't need email
    assert "recommendations" not in result  # mobile doesn't get recs
    assert result["recent_order_count"] == 2
    print(f"PASS: mobile dashboard: {result}")


def test_web_bff_rich_payload():
    bff = WebBFF(UserService(), OrderService(), RecommendationService())
    result = bff.get_dashboard("U1")

    assert "user" in result and "email" in result["user"]
    assert "orders" in result and len(result["orders"]) == 3
    assert "recommendations" in result and len(result["recommendations"]) == 2
    assert "order_total" in result
    print(f"PASS: web dashboard keys: {list(result.keys())}")
    print(f"      user fields: {list(result['user'].keys())}")
    print(f"      order count: {len(result['orders'])}, total: ${result['order_total']:.2f}")


def test_mobile_vs_web_payload_size():
    mobile = MobileBFF(UserService(), OrderService()).get_dashboard("U1")
    web = WebBFF(UserService(), OrderService(), RecommendationService()).get_dashboard("U1")

    mobile_size = len(str(mobile))
    web_size = len(str(web))
    assert mobile_size < web_size
    print(f"PASS: mobile payload ({mobile_size} chars) < web payload ({web_size} chars)")


def test_unknown_user():
    mobile = MobileBFF(UserService(), OrderService()).get_dashboard("UNKNOWN")
    assert "error" in mobile
    print(f"PASS: unknown user -> {mobile}")


if __name__ == "__main__":
    test_mobile_bff_minimal_payload()
    test_web_bff_rich_payload()
    test_mobile_vs_web_payload_size()
    test_unknown_user()
    print("\nAll Backend for Frontend tests passed.")
```

## JavaScript

```javascript
// backend_for_frontend.js — BFF pattern in JavaScript

// ── Simulated backend microservices ──────────────────────────

class UserService {
  constructor() {
    this._users = {
      U1: {
        id: "U1",
        name: "Alice",
        email: "alice@example.com",
        avatar: "https://img.example.com/alice.png",
        bio: "Software engineer",
        joined: "2023-01-15",
      },
    };
  }

  getFull(userId) {
    return this._users[userId] || null;
  }

  getSummary(userId) {
    const user = this._users[userId];
    return user ? { id: user.id, name: user.name } : null;
  }
}

class OrderService {
  constructor() {
    this._orders = {
      U1: [
        {
          id: "O1",
          item: "Widget",
          total: 29.99,
          status: "shipped",
          date: "2024-03-01",
        },
        {
          id: "O2",
          item: "Gadget",
          total: 49.99,
          status: "delivered",
          date: "2024-02-15",
        },
        {
          id: "O3",
          item: "Doohickey",
          total: 14.99,
          status: "processing",
          date: "2024-03-10",
        },
      ],
    };
  }

  getAll(userId) {
    return this._orders[userId] || [];
  }

  getRecent(userId, limit = 3) {
    return (this._orders[userId] || []).slice(0, limit);
  }
}

class RecommendationService {
  get(userId) {
    return [
      { id: "P1", name: "Thingamajig", price: 19.99 },
      { id: "P2", name: "Whatchamacallit", price: 34.99 },
    ];
  }
}

// ── BFF for Mobile ───────────────────────────────────────────

class MobileBFF {
  constructor(userSvc, orderSvc) {
    this.userSvc = userSvc;
    this.orderSvc = orderSvc;
  }

  getDashboard(userId) {
    const user = this.userSvc.getSummary(userId);
    if (!user) return { error: "user not found" };

    const recent = this.orderSvc.getRecent(userId, 2);
    return {
      name: user.name,
      recentOrderCount: recent.length,
      lastOrderStatus: recent.length > 0 ? recent[0].status : null,
    };
  }
}

// ── BFF for Web ──────────────────────────────────────────────

class WebBFF {
  constructor(userSvc, orderSvc, recSvc) {
    this.userSvc = userSvc;
    this.orderSvc = orderSvc;
    this.recSvc = recSvc;
  }

  getDashboard(userId) {
    const user = this.userSvc.getFull(userId);
    if (!user) return { error: "user not found" };

    const orders = this.orderSvc.getAll(userId);
    const recommendations = this.recSvc.get(userId);
    const orderTotal = orders.reduce((sum, o) => sum + o.total, 0);

    return { user, orders, orderTotal, recommendations };
  }
}

// ── Tests ────────────────────────────────────────────────────

function testMobileBffMinimalPayload() {
  const bff = new MobileBFF(new UserService(), new OrderService());
  const result = bff.getDashboard("U1");

  console.assert(result.name !== undefined);
  console.assert(result.email === undefined); // mobile doesn't need email
  console.assert(result.recommendations === undefined);
  console.assert(result.recentOrderCount === 2);
  console.log(`PASS: mobile dashboard: ${JSON.stringify(result)}`);
}

function testWebBffRichPayload() {
  const bff = new WebBFF(
    new UserService(),
    new OrderService(),
    new RecommendationService()
  );
  const result = bff.getDashboard("U1");

  console.assert(result.user && result.user.email);
  console.assert(result.orders && result.orders.length === 3);
  console.assert(result.recommendations && result.recommendations.length === 2);
  console.assert(result.orderTotal !== undefined);
  console.log(`PASS: web dashboard keys: ${Object.keys(result)}`);
  console.log(`      user fields: ${Object.keys(result.user)}`);
  console.log(
    `      order count: ${result.orders.length}, total: $${result.orderTotal.toFixed(2)}`
  );
}

function testMobileVsWebPayloadSize() {
  const mobile = new MobileBFF(
    new UserService(),
    new OrderService()
  ).getDashboard("U1");
  const web = new WebBFF(
    new UserService(),
    new OrderService(),
    new RecommendationService()
  ).getDashboard("U1");

  const mobileSize = JSON.stringify(mobile).length;
  const webSize = JSON.stringify(web).length;
  console.assert(mobileSize < webSize);
  console.log(
    `PASS: mobile payload (${mobileSize} chars) < web payload (${webSize} chars)`
  );
}

function testUnknownUser() {
  const bff = new MobileBFF(new UserService(), new OrderService());
  const result = bff.getDashboard("UNKNOWN");
  console.assert(result.error !== undefined);
  console.log(`PASS: unknown user -> ${JSON.stringify(result)}`);
}

testMobileBffMinimalPayload();
testWebBffRichPayload();
testMobileVsWebPayloadSize();
testUnknownUser();
console.log("\nAll Backend for Frontend tests passed.");
```

## Related Patterns

- **API Gateway** — the BFF is a specialised API Gateway owned by and optimised for a single client type.
- **Facade** — the BFF is a facade over multiple microservices, aggregating and shaping data for its client.
- **CQRS** — the BFF exposes tailored read models per client; this aligns with CQRS's principle of separate read and write models.
