CMD Guide
HomeSystem DesignMicroservices Patterns

BFF Pattern An Example

BFF by example: one product, three payloads

An e-commerce app serves two very different clients — a desktop web frontend and a data-conscious mobile app — both backed by a single Product Service. The naive design lets each client call that service directly and receive the full Product aggregate. The Backend-for-Frontend (BFF) design instead places a thin, client-shaped service in front of the backend for each client type, so every client gets exactly the fields it renders and nothing more.

Below we build all three services in Java and Go, then measure the actual bytes on the wire to see precisely what the pattern buys — and where the savings come from.

diagram
diagram

1. Product Service — the fat backend

One microservice owns product data and returns the entire aggregate: full description, specs, image set, inventory, every review, Q&A, cross-sell IDs. It knows nothing about who is asking, so it hands back everything to everyone.

Java (Spring):

@RestController
@RequestMapping("/products")
public class ProductServiceController {

  @Autowired
  private ProductRepository productRepository;

  // Returns the FULL product aggregate — every field, every review.
  @GetMapping("/{productId}")
  public Product getProductById(@PathVariable String productId) {
    return productRepository.findById(productId)
        .orElseThrow(() -> new ProductNotFoundException(productId));
  }
}

Go (chi):

// GET /products/{id} — full product aggregate, unfiltered.
func (s *ProductServer) GetProduct(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    p, err := s.repo.FindByID(r.Context(), id)
    if err != nil {
        http.Error(w, "product not found", http.StatusNotFound)
        return
    }
    _ = json.NewEncoder(w).Encode(p) // the fat object
}

2. Web BFF — rich, but curated

The desktop page shows a lot: a trimmed marketing description, the top specs, a small gallery, a rating summary, the top handful of reviews, and cross-sell cards. The Web BFF calls the Product Service and projects the fat object down to just that.

Java (Spring):

@RestController
@RequestMapping("/web/products")
public class WebProductController {

  @Autowired
  private ProductServiceClient productServiceClient;

  @GetMapping("/{productId}")
  public WebProductDetails getProductDetails(@PathVariable String productId) {
    Product product = productServiceClient.getProductById(productId);
    return toWebView(product); // trims to what the web page renders
  }

  private WebProductDetails toWebView(Product p) {
    return WebProductDetails.builder()
        .id(p.getId())
        .name(p.getName())
        .price(p.getDisplayPrice())
        .description(truncate(p.getDescription(), 600))
        .specs(topN(p.getSpecs(), 10))
        .gallery(webSized(p.getImages(), 5))
        .rating(p.getRatingSummary())
        .topReviews(p.getReviews().stream().limit(5).toList())
        .related(p.getRelated().stream().limit(6).toList())
        .build();
  }
}

Go (chi):

// GET /web/products/{id} — rich view for the desktop page.
func (b *WebBFF) GetProductDetails(w http.ResponseWriter, r *http.Request) {
    p, err := b.client.GetProduct(r.Context(), chi.URLParam(r, "id"))
    if err != nil { http.Error(w, "upstream error", http.StatusBadGateway); return }

    view := WebProductDetails{
        ID:          p.ID,
        Name:        p.Name,
        Price:       p.DisplayPrice,
        Description: truncate(p.Description, 600),
        Specs:       topN(p.Specs, 10),
        Gallery:     webSized(p.Images, 5),
        Rating:      p.RatingSummary,
        TopReviews:  firstN(p.Reviews, 5),
        Related:     firstN(p.Related, 6),
    }
    _ = json.NewEncoder(w).Encode(view)
}

3. Mobile BFF — only the card

The mobile list/detail card shows a name, price, one thumbnail, an in-stock flag, and a star rating. Everything else is dropped. The Mobile BFF calls the same Product Service and returns a tiny shape.

Java (Spring):

@RestController
@RequestMapping("/mobile/products")
public class MobileProductController {

  @Autowired
  private ProductServiceClient productServiceClient;

  @GetMapping("/{productId}")
  public MobileProductCard getProductDetails(@PathVariable String productId) {
    Product product = productServiceClient.getProductById(productId);
    return toMobileCard(product); // only what the card shows
  }

  private MobileProductCard toMobileCard(Product p) {
    return MobileProductCard.builder()
        .id(p.getId())
        .name(p.getName())
        .price(p.getDisplayPrice())
        .thumbnail(thumb(p.getImages()))
        .inStock(p.getInventory().isAvailable())
        .rating(p.getRatingSummary())
        .build();
  }
}

Go (chi):

// GET /mobile/products/{id} — the bytes a phone actually needs.
func (b *MobileBFF) GetProductCard(w http.ResponseWriter, r *http.Request) {
    p, err := b.client.GetProduct(r.Context(), chi.URLParam(r, "id"))
    if err != nil { http.Error(w, "upstream error", http.StatusBadGateway); return }

    card := MobileProductCard{
        ID:      p.ID,
        Name:    p.Name,
        Price:   p.DisplayPrice,
        Thumb:   thumb(p.Images),
        InStock: p.Inventory.Available,
        Rating:  p.RatingSummary,
    }
    _ = json.NewEncoder(w).Encode(card)
}

Why bother? A byte-level trace

Here is the same product, serialized as JSON three ways, field group by field group. A dash (—) means the field is omitted, i.e. 0 B. The overhead row is JSON structural characters plus header bytes on a warm HTTP/2 connection (HPACK-indexed; see the note under the table). Every column adds up to its own TOTAL — add any column yourself and check.

Field groupRaw Product (B)Web BFF (B)Mobile BFF (B)
Identity (id, name, brand)21012060
Pricing (price, tax, discounts)2609020
Marketing description2,400640
Structured specs1,600520
Images1,80042070
Inventory / availability1,1004012
Seller / merchant profile700
Rating summary (avg + count)606026
Reviews (50 bodies + metadata)21,0001,500
Q&A section2,200
Related / cross-sell1,600900
SEO & analytics metadata900
Protocol + JSON framing overhead23011012
TOTAL ON THE WIRE34,060 (~34 KB)4,400 (~4.4 KB)200 (~0.20 KB)

Reading the totals against each other — every ratio below comes straight from the TOTAL row:

diagram
diagram

How the trace was built (and why reviews dominate)

Bytes are UTF-8 JSON, counted as listed field-group bytes + one overhead row = the column TOTAL. The overhead row counts JSON structural punctuation plus header bytes as seen on a warm HTTP/2 connection, where HPACK indexes repeated response headers down to a few bytes. On a cold HTTP/1.1 exchange add roughly 100 B of uncompressed headers to every column — which softens the mobile ratio from ~170× to ~114× (34,160 ÷ 300) but changes no conclusion.

The single biggest line is reviews: 21,000 B — 50 reviews at roughly 420 B each. That 420 B is a real review: a ~300-character body plus its rating, author, date, and JSON keys (~120 B of metadata). Reviews alone are ~62% of the raw payload, which is exactly why trimming them to the top 5 on web (and dropping them entirely on mobile) is where most of the savings come from.

This also shows the premise is conservative, not inflated: real catalogs carry reviews of 100–500 characters, and hundreds of them on popular items. Push review count or body length up and the raw object grows into the tens or hundreds of KB while the mobile card stays at ~200 B — the gap widens, it never shrinks. The BFF is doing the projection your client would otherwise pay for on every request.

Pitfalls to watch

When to use it — and when not to

Reach for a BFF when:

Prefer an alternative — do NOT add a BFF — when:

The core trade-off vs. the named alternative: an API gateway centralizes cross-cutting concerns (auth, rate-limiting, routing) in one place but forces every client onto a lowest-common-denominator contract; a BFF gives each client a tailored, minimal contract — the payoff measured above — at the cost of more services to build, deploy, and keep in sync. Choose the BFF when per-client divergence is real and worth that operational overhead; choose the gateway (or GraphQL) when it is not.

Source & further reading

The Java Product Service / Web BFF / Mobile BFF example is expanded from the guide's original lesson; the byte-level trace, the Go implementations, the diagrams, and the selection/trade-off analysis are added for depth. All byte figures are illustrative but internally consistent (each column sums to its stated total).

🤖 Don't fully get this? Learn it with Claude

Stuck on BFF Pattern An Example? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **BFF Pattern An Example** (System Design) and want to truly understand it. Explain BFF Pattern An Example from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **BFF Pattern An Example** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **BFF Pattern An Example** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **BFF Pattern An Example** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes