A Backend for Frontend (BFF) is a small server-side application that exists for exactly one kind of client (for example the Angular web app, the iOS/Android app, or a partner portal).
Intro
A Backend for Frontend (BFF) is a small server-side application that exists for exactly one kind of client (for example the Angular web app, the iOS/Android app, or a partner portal). It sits between that client and the internal microservices, and shapes data, security and payloads to that client’s needs. Instead of one big shared gateway that tries to please everybody, each client type gets its own thin, purpose-built backend, owned by the team that builds that client.
Why we need this
Microservices are designed around business capabilities (Claims, Policies, Payments, Documents), not around screens. A single screen, such as “Claim details”, needs data from four or five of them. Different clients also need different things from the same data:
- The Angular claims-handler web app needs a rich view: claim, policy, payments, documents, audit trail, on a wide screen with a good network.
- The mobile app for policyholders needs a slim view: status, next action, last three documents, over a flaky mobile network with a strict payload budget.
- A partner/broker portal needs a stable, versioned, contract-first view with different authorization rules.
Without a BFF, either the client makes many calls and stitches results itself (chatty, slow, leaks internal topology), or the shared API/gateway grows client-specific endpoints and flags until it becomes a bottleneck owned by nobody. A BFF gives each frontend team its own backend so they can change screens and their API in one pull request.
A second, security-driven reason is now equally important: browser apps (SPAs) are poor places to hold OAuth tokens. A BFF can run the OpenID Connect flow server-side, keep tokens out of the browser, and give the SPA only an HttpOnly, Secure, SameSite session cookie. This is the recommended pattern in the IETF draft “OAuth 2.0 for Browser-Based Applications”.
What problem it solves
Problem (from the topic list): one shared gateway bloats when serving mobile, web and IoT clients.
What goes wrong without it, in the insurance claims system:
- The shared gateway gets endpoints like
/claims/{id}/mobile-summary,/claims/{id}/web-full,/claims/{id}/broker-view. Each change needs coordination between three client teams and the gateway team. - Query-string flags (
?fields=...&client=ios) andif (userAgent == ...)branches appear. Nobody can reason about the behaviour any more. - The mobile app downloads 180 KB of JSON to render a 2 KB status card.
- The Angular app calls six services per screen, so the page needs six round trips and six sets of error handling.
- Access tokens (with refresh tokens) sit in
localStorage, exposed to any XSS bug. - A release of one client’s needs blocks the others because the gateway is one deployable.
The BFF solves this by giving each client type its own thin backend: aggregating calls, trimming and reshaping payloads, handling that client’s authentication flow, and evolving at the pace of that client.
When it is needed (and when it is NOT)
Fits well when:
- You have two or more materially different client types (web, mobile, partner API, kiosk/IoT) with different data shapes, payload budgets or auth flows.
- A screen needs data from multiple services and you want one call per screen.
- The frontend team is a separate, cross-functional team that should own its backend contract (see Day 4, Service per Team).
- You have a SPA and want to keep tokens out of the browser (token-mediating / cookie-based BFF).
- The shared API gateway is already turning into a dumping ground for client-specific logic.
Overkill or wrong when:
- You have a single client and one or two services. A well-designed API and the frontend calling it directly is simpler.
- The “BFF” would only forward requests unchanged. That is just another reverse proxy; use the API Gateway (Day 19) or a plain YARP config.
- You want to put business rules in it. Domain logic belongs in domain services; a BFF that owns rules becomes a second, unofficial monolith.
- Your clients are almost identical. One shared API with sparse fieldsets or GraphQL may be enough.
- You do not have a team to own each BFF. An orphaned BFF is worse than none.
How to identify the problem (key signals)
- Endpoint names in the gateway contain client names or screen names (
mobile-summary,dashboard-v2-ios). - The Angular page loads and the browser network tab shows 5+ sequential or parallel API calls just to render one screen.
- Mobile crash or timeout reports on poor networks, with response payloads far larger than what the UI renders (check p95 response size per client in Application Insights).
- Pull requests to the gateway repository need reviewers from several client teams; lead time for “add one field to one screen” is measured in weeks.
- Access or refresh tokens visible in browser storage during a security review or penetration test.
- The same aggregation and mapping code is copy-pasted in the Angular services and the Android/iOS apps, and the copies drift.
- Feature flags or
User-Agent/X-Clientheader checks inside shared API code.
Flow Diagram
Each client type gets its own thin backend that aggregates and shapes data for it.
flowchart LR WEB["Angular handler portal"] -- "session cookie" --> WBFF["Web BFF: OIDC, aggregation"] MOB["Policyholder mobile app"] --> MBFF["Mobile BFF: slim payloads"] WBFF -- "bearer token" --> CL["Claims"] WBFF --> PA["Payments"] WBFF --> DO["Documents"] MBFF --> CL MBFF --> PO["Policies"]Level 1: Beginner
Analogy: a restaurant. The kitchen (microservices) cooks individual dishes. A waiter for the dining room (web BFF) and a separate takeaway counter (mobile BFF) each assemble trays in the way their customers want. Customers never walk into the kitchen and never need to know which chef made what.
Minimal example: a BFF endpoint for the web app that returns one combined “claim summary” by calling two internal services. .NET 10 minimal API:
// Program.cs - Claims.WebBffvar builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("claims", c => c.BaseAddress = new Uri(builder.Configuration["Services:Claims"]!));builder.Services.AddHttpClient("policies", c => c.BaseAddress = new Uri(builder.Configuration["Services:Policies"]!));
var app = builder.Build();
app.MapGet("/bff/claims/{id:guid}/summary", async ( Guid id, IHttpClientFactory factory, CancellationToken ct) =>{ var claims = factory.CreateClient("claims"); var policies = factory.CreateClient("policies");
var claim = await claims.GetFromJsonAsync<ClaimDto>($"/api/claims/{id}", ct); if (claim is null) return Results.NotFound();
var policy = await policies.GetFromJsonAsync<PolicyDto>( $"/api/policies/{claim.PolicyId}", ct);
return Results.Ok(new ClaimSummaryVm( claim.Id, claim.Status, claim.AmountClaimed, policy?.HolderName ?? "Unknown", policy?.ProductName ?? "Unknown"));});
app.Run();
public record ClaimDto(Guid Id, Guid PolicyId, string Status, decimal AmountClaimed);public record PolicyDto(Guid Id, string HolderName, string ProductName);public record ClaimSummaryVm(Guid Id, string Status, decimal AmountClaimed, string HolderName, string ProductName);The Angular app now makes one call instead of two, and receives exactly the fields the screen renders (ClaimSummaryVm), not the internal DTOs.
Level 2: Intermediate
In a real .NET + Angular + SQL Server/PostgreSQL system the BFF does four jobs: (1) authentication for the SPA, (2) aggregation, (3) proxying simple calls, (4) shaping responses. Note the BFF itself normally has no database of its own; state lives in the services. (Session data for scale-out can go in a distributed cache such as Redis.)
6.1 Cookie + OpenID Connect (tokens never reach the browser)
// Program.cs (Claims.WebBff) - packages:// Microsoft.AspNetCore.Authentication.OpenIdConnect, Yarp.ReverseProxyusing Microsoft.AspNetCore.Authentication;using Microsoft.AspNetCore.Authentication.Cookies;using Microsoft.AspNetCore.Authentication.OpenIdConnect;using Yarp.ReverseProxy.Transforms;
var builder = WebApplication.CreateBuilder(args);
builder.Services .AddAuthentication(o => { o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie(o => { o.Cookie.Name = "__Host-claims-bff"; o.Cookie.HttpOnly = true; o.Cookie.SecurePolicy = CookieSecurePolicy.Always; o.Cookie.SameSite = SameSiteMode.Strict; // API calls should get 401, not a redirect to the login page o.Events.OnRedirectToLogin = ctx => { if (ctx.Request.Path.StartsWithSegments("/bff") || ctx.Request.Path.StartsWithSegments("/api")) { ctx.Response.StatusCode = StatusCodes.Status401Unauthorized; return Task.CompletedTask; } ctx.Response.Redirect(ctx.RedirectUri); return Task.CompletedTask; }; }) .AddOpenIdConnect(o => { o.Authority = builder.Configuration["Auth:Authority"]; // Entra ID tenant / issuer o.ClientId = builder.Configuration["Auth:ClientId"]; o.ClientSecret = builder.Configuration["Auth:ClientSecret"]; // from Key Vault o.ResponseType = "code"; // authorization code flow with PKCE o.UsePkce = true; o.SaveTokens = true; // tokens stay in the server-side auth ticket o.Scope.Add(builder.Configuration["Auth:ApiScope"]!); });
builder.Services.AddAuthorization();builder.Services.AddAntiforgery(o => o.HeaderName = "X-XSRF-TOKEN");
builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) .AddTransforms(ctx => { // Swap the session cookie for a bearer token on the way to the service ctx.AddRequestTransform(async t => { var token = await t.HttpContext.GetTokenAsync("access_token"); if (token is not null) t.ProxyRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); t.ProxyRequest.Headers.Remove("Cookie"); // never leak the session cookie downstream }); });
var app = builder.Build();
app.UseAuthentication();app.UseAuthorization();
app.MapGet("/bff/login", (string? returnUrl) => Results.Challenge(new AuthenticationProperties { RedirectUri = returnUrl ?? "/" }));app.MapPost("/bff/logout", () => Results.SignOut(new AuthenticationProperties { RedirectUri = "/" }, [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme]));app.MapGet("/bff/user", (HttpContext http) => http.User.Identity?.IsAuthenticated == true ? Results.Ok(http.User.Claims.Select(c => new { c.Type, c.Value })) : Results.Unauthorized());
// Simple pass-through routes (no aggregation needed) via YARPapp.MapReverseProxy().RequireAuthorization();
app.Run();appsettings.json routes for the pass-through part:
{ "ReverseProxy": { "Routes": { "documents": { "ClusterId": "documents", "Match": { "Path": "/api/documents/{**catch-all}" } } }, "Clusters": { "documents": { "Destinations": { "d1": { "Address": "https://documents.internal/" } } } } }}6.2 Aggregation with parallel calls and partial failure handling
// Program.cs (continued) - typed clients with the standard resilience pipelinebuilder.Services.AddHttpClient<ClaimsClient>(c => c.BaseAddress = new Uri(builder.Configuration["Services:Claims"]!)) .AddStandardResilienceHandler(); // retry, circuit breaker, timeoutsbuilder.Services.AddHttpClient<PaymentsClient>(c => c.BaseAddress = new Uri(builder.Configuration["Services:Payments"]!)) .AddStandardResilienceHandler();builder.Services.AddHttpClient<DocumentsClient>(c => c.BaseAddress = new Uri(builder.Configuration["Services:Documents"]!)) .AddStandardResilienceHandler();
app.MapGet("/bff/claims/{id:guid}/details", async ( Guid id, ClaimsClient claims, PaymentsClient payments, DocumentsClient docs, CancellationToken ct) =>{ var claimTask = claims.GetAsync(id, ct); // critical var paymentsTask = payments.ListForClaimAsync(id, ct); // optional var docsTask = docs.ListForClaimAsync(id, ct); // optional
var claim = await claimTask; if (claim is null) return Results.NotFound();
// Optional panels degrade instead of failing the whole page var paymentList = await TryAsync(paymentsTask); var docList = await TryAsync(docsTask);
return Results.Ok(new ClaimDetailsVm( claim, paymentList ?? [], docList ?? [], Degraded: paymentList is null || docList is null));
static async Task<IReadOnlyList<T>?> TryAsync<T>(Task<IReadOnlyList<T>> t) { try { return await t; } catch (HttpRequestException) { return null; } catch (TaskCanceledException) { return null; } }}).RequireAuthorization();The ClaimsClient, PaymentsClient and DocumentsClient are small typed wrappers around HttpClient. The important design point is that the BFF decides which data is critical (claim) and which can degrade (payments, documents), because that is a screen-level decision that only the client’s team can make.
6.3 Angular side (current Angular, standalone components, signals)
import { Injectable, inject } from '@angular/core';import { HttpClient } from '@angular/common/http';
export interface ClaimDetailsVm { claim: { id: string; status: string; amountClaimed: number }; payments: { id: string; amount: number; paidOn: string }[]; documents: { id: string; name: string }[]; degraded: boolean;}
@Injectable({ providedIn: 'root' })export class ClaimDetailsService { private http = inject(HttpClient); // same-origin: the BFF also serves/proxies the SPA, so the cookie is sent automatically get(id: string) { return this.http.get<ClaimDetailsVm>(`/bff/claims/${id}/details`); }}// csrf.interceptor.ts - functional interceptorimport { HttpInterceptorFn } from '@angular/common/http';
export const bffInterceptor: HttpInterceptorFn = (req, next) => { // Custom header forces a CORS preflight for cross-site requests -> simple CSRF defence if (req.url.startsWith('/bff') || req.url.startsWith('/api')) { req = req.clone({ setHeaders: { 'X-CSRF': '1' } }); } return next(req);};On the BFF, reject state-changing requests (POST/PUT/PATCH/DELETE) that lack the X-CSRF header (a tiny middleware), in addition to SameSite=Strict.
Database note: the BFF does not query SQL Server or PostgreSQL directly. If you find yourself adding an EF Core DbContext to a BFF, stop and ask which service should own that data (Day 5, Database per Service).
Level 3: Advanced
Performance and scalability
- Fan out in parallel (
Task.WhenAllor start tasks first, then await), never sequentially, unless a call depends on another’s output. Total latency should be roughly the slowest call, not the sum. - Set per-call timeouts shorter than the client’s own timeout.
AddStandardResilienceHandlergives sensible defaults (total request timeout, retry, circuit breaker, attempt timeout); tune them per dependency. - Cache read-mostly reference data (product catalogue, code lists) with
HybridCache(available in .NET 9 and later, so in .NET 10 LTS) backed by Redis, to avoid hitting services for identical data. - Keep the BFF stateless when possible. If you use the cookie session with tokens saved in the cookie ticket, large tokens make cookies big (chunked cookies, 4 KB limit per cookie). For scale-out and smaller cookies, use a server-side ticket store (
ITicketStore) backed by Redis. - Response shaping: return only fields the screen renders; enable Brotli/gzip response compression; for mobile consider ETags.
Security
- Cookie:
__Host-prefix,HttpOnly,Secure,SameSite=Strict(or Lax if you must support cross-site navigation into the app). Add the CSRF header check for unsafe methods. - Tokens: only the BFF holds access and refresh tokens. Use scopes/audiences per downstream API; for calls that need the user’s identity across services, use the on-behalf-of flow rather than reusing one broad token.
- Authorization is still enforced by the downstream services. A BFF may hide a button, but it must never be the only place that decides whether a claims adjuster can approve a payment.
- Do not expose internal error details. Map downstream failures to a small, stable problem-details response.
- Treat the BFF as a public-facing component: WAF in front, rate limiting (Day 30), and request-size limits.
Failure modes
- One slow dependency stalls every page that aggregates it. Fix with timeouts, circuit breakers (Day 26), bulkheads (Day 27) and optional panels that degrade.
- Thundering herd on retries during an incident: use exponential backoff with jitter (Day 28); avoid stacking retries at the client, the BFF and the service all at once.
- Token expiry mid-session: refresh tokens server-side and return 401 to the SPA only when the session is truly over; the SPA then redirects to
/bff/login. - Session loss when scaled out without a shared ticket store (users randomly logged out). Use Redis for tickets and Data Protection keys (persist keys in Blob Storage + Key Vault).
Common mistakes
- Putting business rules in the BFF (claim approval limits, premium calculations).
- One “universal” BFF for all clients. That is just the shared gateway again with a different name.
- A BFF per screen. Too fine; it becomes a nano-service explosion. Aim for one per client type or per client team.
- The BFF owning a database.
- Passing downstream DTOs straight through so every internal change breaks the client.
- No ownership: the BFF sits with a platform team that does not know the client’s needs. The frontend team should own it.
- Duplicating aggregation logic across BFFs instead of pushing shared logic down into a real service or shared library.
Level 4: Expert and Architect view
Alternatives compared
| Option | Best for | Strengths | Weaknesses |
|---|---|---|---|
| BFF per client type | 2+ different clients, frontend teams with autonomy | Client-optimised payloads, team ownership, token handling for SPA | More deployables, some duplicated aggregation, needs ownership discipline |
| Single API Gateway (Day 19) | Uniform cross-cutting concerns for all clients | One entry point, central auth/rate limiting/routing, low duplication | Bloats with client-specific logic, becomes a bottleneck team |
| Client calls services directly | Very small systems, internal tools | Simplest | Chatty, leaks topology, CORS and auth in every service, tokens in browser |
| GraphQL gateway/federation | Many clients with highly variable field needs | Client picks fields, one round trip, strong typing | Query cost control, caching complexity, N+1 in resolvers, operational learning curve |
| API Composition (Day 10) in a service | Backend-to-backend joins | Reusable across clients | Not client-shaped; doesn’t handle browser auth flow |
| Server-side page fragment composition (Day 52) | Server-rendered UIs | Independent UI deployment | Different model; not for SPA/mobile |
Patterns it combines with
- API Gateway (Day 19): typically the gateway (or APIM) sits in front for TLS, WAF, global rate limits and routing to the right BFF; each BFF sits behind it. Do not duplicate the same cross-cutting concern in both.
- Access Token (Day 38): the BFF is where the browser’s session is exchanged for a bearer token.
- API Composition (Day 10): the BFF’s aggregation endpoints are a client-specific form of API composition.
- Circuit Breaker, Retry & Backoff, Bulkhead, Fallback (Days 26-29): essential for every downstream call.
- Distributed Tracing and Health Check API (Days 31, 36): propagate the trace context from the BFF to services; expose readiness/liveness.
- Service per Team (Day 4) and Micro-Frontends (Day 53): a BFF per team or per micro-frontend group keeps ownership aligned.
- Consumer-Driven Contract Tests (Day 51): the BFF is a consumer of services; use Pact so provider changes do not break it.
ADR (architecture review style)
- Title: ADR-020: Introduce a dedicated BFF for the Claims Handler web app and a separate BFF for the Policyholder mobile app.
- Status: Proposed.
- Context: The claims system has five services (Claims, Policies, Payments, Documents, Notifications). The Angular handler portal needs a combined view per screen and currently makes 5-7 calls per page; tokens are stored in browser storage; the shared gateway has three client-specific endpoints and is changed by three teams. The mobile app is planned with a very different payload budget.
- Decision: Create
Claims.WebBff(.NET 10, YARP + minimal APIs, cookie+OIDC) owned by the web team, andClaims.MobileBffowned by the mobile team. Both sit behind Azure Front Door/API Management for WAF and edge policies. BFFs contain no business rules and no databases. Downstream services continue to enforce authorization. - Consequences (positive): fewer client round trips, smaller mobile payloads, no tokens in the browser, faster client-team delivery, gateway stays generic.
- Consequences (negative): two more deployables to run and monitor, some duplicated aggregation, need for contract tests and clear ownership, additional hop adds latency (mitigated by same-region hosting and parallel calls).
- Alternatives rejected: extending the shared gateway (bottleneck, already bloated); GraphQL federation (team has no experience, higher operational cost for now; revisit at 5+ clients).
- Review trigger: revisit if BFFs start to share more than about 30 percent of their aggregation code, or if a third client type appears.
Azure implementation
Services
- Hosting the BFF: Azure Container Apps (good default for .NET 10 containers, scale-to-zero option, KEDA autoscaling, built-in ingress) or Azure App Service (simpler if the team is not container-based) or AKS (if you already run Kubernetes; see Day 46).
- Edge: Azure Front Door (global entry, WAF, TLS, caching of static SPA assets). Optionally Azure API Management in front of the BFFs for policies, subscriptions (mostly for partner-facing BFFs) and central analytics.
- Identity: Microsoft Entra ID (workforce handler portal) or Entra External ID (customers). The BFF is a confidential client with a client secret or, better, a certificate or workload identity (federated credential) instead of a secret.
- Secrets and config: Azure Key Vault + Azure App Configuration; access via managed identity (no secrets in images).
- State: Azure Cache for Redis (or Azure Managed Redis; check current offering and naming) for session tickets, HybridCache and Data Protection key ring coordination; Blob Storage for the Data Protection key file.
- Observability: Application Insights / Azure Monitor with OpenTelemetry (
Azure.Monitor.OpenTelemetry.AspNetCore), so each BFF request shows as the parent of its downstream calls. - Static SPA hosting option: Azure Static Web Apps can host the Angular app and link a backend; alternatively serve the Angular build from the BFF itself (same origin, simplest cookie story).
How to configure (key points)
- Register the BFF as a web app in Entra ID: redirect URI
https://claims.contoso.com/signin-oidc, post-logout URI, expose/consume API scopes for downstream services. - Deploy the BFF to Container Apps in the same region and, ideally, the same virtual network as the services. Use internal ingress for the services and external ingress only for the BFF (or only for Front Door with private link where required).
- Assign a user-assigned managed identity to the BFF; grant it Key Vault secrets user and App Configuration data reader roles.
- Configure Front Door: custom domain and managed certificate, WAF policy in prevention mode, origin group pointing at the BFF, health probe on
/health/ready. - Set scale rules on Container Apps: HTTP concurrency (for example scale out at N concurrent requests per replica), min replicas of at least 1-2 for production BFFs to avoid cold starts on login paths.
- Add Application Insights connection string via Key Vault reference; enable OpenTelemetry tracing and W3C trace-context propagation.
Pricing and tier considerations (verify current prices on the Azure pricing pages before committing)
- Container Apps: consumption plan is billed on vCPU-seconds, memory GiB-seconds and requests with a monthly free grant; dedicated workload profiles are billed per profile instance. Scale-to-zero saves money but adds cold-start latency; keep min replicas at 1 or more for interactive BFFs.
- App Service: billed per plan (Basic, Standard, Premium v3/v4 families), not per app; several BFFs can share one plan in non-production.
- API Management: classic tiers are Consumption, Developer (non-production only), Basic, Standard and Premium; the newer v2 tiers (Basic v2, Standard v2, Premium v2) start faster and cost less than the classic equivalents for many workloads. Consumption is pay-per-call but has feature limits; Developer has no SLA. For a purely internal web BFF, APIM is optional; add it when you need subscriptions, per-product policies or a partner developer portal.
- Front Door: Standard vs Premium (Premium adds Private Link origins and advanced WAF managed rule sets); billed by base fee plus data transfer and requests.
- Redis: pick a tier with an SLA for production (Standard or higher / the managed offering with replication); Basic caches have no replication.
- Application Insights: billed by ingested GB; use sampling for high-traffic BFFs.
Reference architecture (text)
Browser (Angular SPA) and Mobile app -> Azure Front Door (WAF, TLS) -> [optional Azure API Management for partner-facing routes] -> Web BFF (Container App, external ingress, cookie + OIDC) / Mobile BFF (Container App, token-based) -> internal Container Apps environment (VNet) with Claims, Policies, Payments, Documents services on internal ingress -> each service’s own database (Azure SQL / SQL Server for Claims and Payments, Azure Database for PostgreSQL flexible server for Documents metadata) and Azure Service Bus for events. Cross-cutting: Entra ID for sign-in, Key Vault + App Configuration via managed identity, Redis for sessions and cache, Application Insights + Log Analytics for traces, logs and metrics, all connected by W3C trace context.
Teaching guide for my team
Explain to a beginner in 2 minutes
“Our web page needs data from many small services. Instead of the browser calling them all, we build a small server just for that web app. It calls the services, picks the fields the page needs, and sends back one neat answer. The mobile app gets its own small server that sends less data. Each server belongs to the team that builds that app, so they can change it without asking anyone else. It also keeps the login tokens on the server, so the browser only holds a safe cookie.”
Explain to an intermediate developer in 5 minutes
Walk through the problem (chatty pages, bloated gateway, tokens in the browser) using the claim details screen. Draw the request flow: SPA -> BFF (cookie) -> services (bearer token). Show the parallel aggregation with optional panels, point out that failure handling and screen-level decisions live here, and stress what must not live here: business rules, databases, and authorization as the only line of defence. Contrast with the API Gateway: the gateway is shared and generic; the BFF is per client and specific. Finish with the ownership rule: the client team owns the BFF.
Hands-on exercise
Task: Build Claims.WebBff with one endpoint GET /bff/claims/{id}/details that calls two stub services (Claims, Payments) and returns a combined view. Make the Payments stub sleep 5 seconds or return 500 randomly.
Steps: use two HttpClients with AddStandardResilienceHandler; run calls in parallel; treat Payments as optional; return degraded: true when it fails; add cookie authentication with a fake login.
Expected outcome: the endpoint responds in about the time of the slowest critical call (not the sum); when Payments fails the response is still 200 with an empty payments list and degraded: true; when Claims fails the response is 404/502 as appropriate; the session cookie is HttpOnly, and no token appears in any response body.
Interview-style questions
- Q: How is a BFF different from an API Gateway? A: A gateway is a shared, generic entry point for cross-cutting concerns (routing, auth, rate limits) across clients. A BFF is specific to one client type and does aggregation, response shaping and often the client’s auth flow. They are commonly used together, with the gateway in front of the BFFs.
- Q: Why keep OAuth tokens in the BFF instead of the Angular app? A: Tokens in browser storage can be stolen by XSS. The BFF holds them server-side and gives the SPA an HttpOnly, Secure, SameSite cookie, which JavaScript cannot read. You then need CSRF protection because cookies are sent automatically.
- Q: A colleague wants to add claim-approval-limit rules to the BFF because “it is convenient”. What do you say? A: No. The BFF is a presentation-layer backend. Business rules belong in the domain service so every client and every entry point enforces the same rule; otherwise rules get duplicated and drift across BFFs.
Mastery checklist
- I can explain the difference between a BFF, an API Gateway and API Composition, and when each is used.
- I can identify at least four signals in a real system that indicate a BFF is needed (or is not needed).
- I can implement a cookie + OpenID Connect BFF in .NET that forwards a bearer token to downstream services and never exposes tokens to the browser.
- I can write parallel aggregation with timeouts, retries and graceful degradation for optional data.
- I can defend CSRF, cookie and session-scaling choices (SameSite, custom header, Redis ticket store, Data Protection keys).
- I can decide what belongs in the BFF and what must stay in domain services.
- I can host and secure a BFF on Azure (Container Apps, Front Door, Entra ID, Key Vault, Application Insights) and describe the cost drivers.
- I can write an ADR justifying a BFF per client type versus one shared gateway or GraphQL.
Key takeaway
Give each client type its own thin, team-owned backend that aggregates and shapes data for that client and holds the tokens, and keep business rules in the services. A BFF removes client-specific clutter from the shared gateway; it must never become a second monolith.
