Waqas Ahmad — Software Architect & Technical Consultant - Available USA, Europe, Global

Waqas Ahmad — Software Architect & Technical Consultant

Specializing in

Distributed Systems

.NET ArchitectureCloud-Native ArchitectureAzure Cloud EngineeringAPI ArchitectureMicroservices ArchitectureEvent-Driven ArchitectureDatabase Design & Optimization

👋 Hi, I'm Waqas — a Software Architect and Technical Consultant specializing in .NET, Azure, microservices, and API-first system design..
I help companies build reliable, maintainable, and high-performance backend platforms that scale.

Experienced across engineering ecosystems shaped by Microsoft, the Cloud Native Computing Foundation, and the Apache Software Foundation.

Available for remote consulting (USA, Europe, Global) — flexible across EST, PST, GMT & CET.

services
Article

API Gateway vs BFF: When to Use Which

API Gateway and BFF: architecture, when to use which, and implementation with APIM or.

services
Read the article

Introduction

This guidance is relevant when you have multiple backend services and need a single entry point for clients—routing, auth, and optionally aggregation. It breaks down when you have a single monolith or when client types are so uniform that one API suffices. I’ve applied Gateway + BFF in contexts where web, mobile, and partner clients needed different shapes of the same data and where the platform wanted one place for auth and rate limits (as of 2026).

As systems grow with microservices, exposing each service directly leads to many client–service connections, repeated auth and rate-limiting logic, and clients orchestrating multiple calls. This article explains API Gateway and BFF (Backend-for-Frontend): what each is, when to use which, how they fit together, and implementation with Azure APIM, Ocelot, and ASP.NET Core BFF. For architects and tech leads, choosing Gateway-only versus Gateway + BFF affects clarity of ownership, cross-cutting policy in one place, and client-specific aggregation without overloading the edge.

If you are new to the topic, start with Topics covered and What is an API Gateway?.

For a deeper overview of this topic, explore the full API Architecture guide.


Decision Context

  • System scale: Multiple backend services (typically 3+); clients are web, mobile, or partner APIs. Single-digit to low double-digit service count is the sweet spot; at very high scale, gateway and BFF ownership and versioning become the bottleneck.
  • Team size: Gateway usually owned by platform/infrastructure; BFF by the frontend or client team. Works when at least one team can own the BFF and coordinate with backend owners.
  • Time / budget pressure: Gateway-first (e.g. Azure APIM, Ocelot) gets you auth and routing quickly; adding a BFF is a separate delivery step. I’ve seen BFF introduced after the gateway when dashboards or mobile needed aggregation.
  • Technical constraints: Backends must be callable from the BFF (network, auth). If backends are only internal or legacy, BFF may need adapters or a different placement.
  • Non-goals: This split does not optimize for minimal latency (extra hop) or for “one API to rule them all”—it optimizes for clear ownership, cross-cutting policy in one place, and client-specific aggregation without bloating the gateway.

What is an API Gateway?

An API Gateway is a single entry point for client requests to your system. It sits at the edge (between clients and your backends) and handles routing, authentication, rate limiting, SSL termination, caching, and monitoring—without running your business logic or aggregating data from multiple services.

Responsibility Description
Routing Forward each request to the appropriate backend (service or BFF) based on path, host, or policy.
Authentication Validate JWT, API keys, or cookies; reject unauthenticated requests (401).
Authorization Enforce policies (e.g. scope, role) before forwarding; return 403 if not allowed.
Rate limiting Throttle requests per client, API, or endpoint; return 429 when limit exceeded.
CORS Add CORS headers and handle preflight (OPTIONS) so browsers can call your API.
SSL termination Terminate HTTPS at the gateway; optional TLS to backends.
Caching Cache responses by key (e.g. path + query) to reduce load on backends.
Monitoring Log requests, collect metrics (latency, errors), and optionally trace.

Key point: The gateway typically does not aggregate data or implement business logic. It routes and applies cross-cutting policies. Your backends (services or BFFs) do the work.

Examples: Azure API Management, AWS API Gateway, Kong, NGINX, Ocelot (.NET).


What is a BFF?

A BFF (Backend-for-Frontend) is a backend tailored to a specific client type (web SPA, mobile app, partner API). It aggregates data from multiple backend services, transforms responses for that client, and exposes one API that matches what that client needs—so the client makes one call instead of many.

Responsibility Description
Aggregation Call multiple services (orders, customers, analytics) and combine results into one response.
Transformation Shape payloads for the client (e.g. smaller for mobile, different structure for web).
Client-specific logic Handle device- or channel-specific needs (e.g. offline support, different fields).
Reduce round-trips One BFF call instead of N client calls to N services; fewer latency and complexity.

Key point: One BFF per client type. Web BFF serves the SPA; Mobile BFF serves the app; Partner BFF serves third parties. Each BFF knows what its client needs and calls the right backends.

Examples: ASP.NET Core API that calls Order Service, Customer Service, Analytics Service and returns a single DashboardDto for the web app.


Architecture: Gateway only vs Gateway + BFF

Gateway only (no BFF): Clients call the gateway; the gateway routes to backend services directly. Each client request maps to one service. Use when clients are simple (one service per request) and do not need aggregation.

[Web] ---> [API Gateway] ---> [Order Service]
[Mobile] ---> [API Gateway] ---> [Order Service], [Customer Service] (separate calls)

Gateway + BFF: Clients call the gateway; the gateway routes to BFFs (or to services). The BFF then calls multiple services and returns one aggregated response. Use when clients need data from many services in one call.

[Web App]  ---> [API Gateway] ---> [Web BFF]  ---> [Order Svc], [Customer Svc], [Analytics Svc]
[Mobile]   ---> [API Gateway] ---> [Mobile BFF] ---> [Order Svc], [Customer Svc]
[Partner]  ---> [API Gateway] ---> [Partner API] ---> [Order Svc]

How this fits together: The gateway is the single entry point: auth, rate limit, route. The BFF is behind the gateway (or exposed through it) and does aggregation. Clients that need a dashboard (many services) call the Web BFF once; the BFF calls the services and returns one DTO. Clients that need a single resource (e.g. one order) can call a route that the gateway sends to the Order Service directly, or to a BFF that proxies—depending on your routing design.


API Gateway vs BFF comparison

Aspect API Gateway BFF
Purpose Routing, auth, rate limiting, policies Aggregation, transformation for one client type
Scope All clients One client type (web, mobile, partner)
Logic Cross-cutting only (no business logic) Client-specific (calls services, shapes response)
Count One (or one per region) One per client type
Ownership Platform / infrastructure team Client / frontend team
Position Edge (first layer) Behind gateway or at edge
Technology Often managed (Azure APIM, AWS) or proxy (Ocelot, Kong) Application (e.g. ASP.NET Core)
Failure handling Returns 502/504 if backend fails Can return partial data, fallback, or cache

When to use API Gateway

Use API Gateway when:

  • You have multiple backends and want a single entry point for clients.
  • You need centralized auth, rate limiting, CORS, and SSL termination.
  • You want to decouple clients from service URLs and deployment details.
  • You need monitoring and logging at the edge (one place for all traffic).

Gateway alone is enough when:

  • Clients call one service per request (no aggregation).
  • All clients have similar needs (same APIs, same auth).
  • You do not need client-specific response shaping (e.g. web vs mobile payloads).

When to add a gateway: As soon as you have more than one backend and want consistent auth, rate limiting, and routing. Even with a single backend, a gateway can simplify SSL, caching, and monitoring.


When to use BFF

Use BFF when:

  • Clients need data from multiple services in one call (e.g. dashboard: orders + customers + analytics).
  • Different client types need different data or shape (web vs mobile vs partner).
  • You want to reduce client complexity (one BFF API instead of N service calls).
  • The frontend team wants to own the API that serves their app (same team owns BFF and UI).

BFF is valuable when:

  • Dashboards aggregate many sources; without BFF the client would make many round-trips.
  • Mobile needs smaller payloads or different fields; BFF shapes the response.
  • SPA needs server-side rendering or a single backend; BFF provides one API.

When to add a BFF: When you have a client (e.g. web app) that repeatedly calls several services to build one screen. Moving that aggregation into a BFF reduces latency, simplifies the client, and lets the client team own the API.


Using both together: request flow

Typical flow when both are used:

  1. Client sends request to API Gateway (e.g. GET /api/web/dashboard).
  2. Gateway validates token (auth), checks rate limit, applies CORS; then routes to Web BFF (e.g. by path prefix /api/web/*).
  3. Web BFF receives request, calls Order Service, Customer Service, Analytics Service (in parallel or as needed), aggregates results, and returns one response (e.g. DashboardDto).
  4. Gateway (optionally) caches the response; returns it to the client.

Gateway responsibilities: Auth, rate limit, route, cache, log. BFF responsibilities: Aggregate, transform, return one DTO. Services: Business logic only; no awareness of client type.

Routing design: Map path prefixes to backends—e.g. /api/web/* → Web BFF, /api/mobile/* → Mobile BFF, /api/orders/* → Order Service (if you want some routes to hit services directly). Keep routing in the gateway; keep aggregation in the BFF.


Implementation: API Gateway (Azure APIM and Ocelot)

Azure API Management

Azure API Management is a managed API Gateway. You define APIs (routes), backends (service URLs), and policies (auth, rate limit, cache). Clients call the APIM URL; APIM forwards to the correct backend.

Structure:

  • Products – Group APIs for subscription/access.
  • APIs – Routes (e.g. /orders, /web/dashboard).
  • Policies – Inbound: validate-jwt, rate-limit; outbound: cache; backend: set URL.
  • Backends – Service or BFF base URLs.

Policy example (inbound: rate limit + JWT):

<policies>
  <inbound>
    <rate-limit calls="100" renewal-period="60" />
    <validate-jwt header-name="Authorization" failed-validation-httpcode="401" />
    <base />
  </inbound>
  <backend>
    <forward-request />
  </backend>
</policies>

Routing: Configure each API to forward to a backend (e.g. Web BFF URL). Path prefix determines which backend receives the request.

Ocelot (.NET)

Ocelot is a .NET API Gateway (library). You run it as an ASP.NET Core app and configure routes and downstream services in JSON (or code). Good for self-hosted or Kubernetes when you do not use Azure APIM.

Example configuration (ocelot.json):

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/api/dashboard",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [{ "Host": "web-bff", "Port": 443 }],
      "UpstreamPathTemplate": "/api/web/dashboard",
      "UpstreamHttpMethod": ["Get"]
    },
    {
      "DownstreamPathTemplate": "/api/orders/{everything}",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [{ "Host": "order-service", "Port": 443 }],
      "UpstreamPathTemplate": "/api/orders/{everything}",
      "UpstreamHttpMethod": ["Get", "Post"]
    }
  ]
}

Program.cs: builder.Configuration.AddJsonFile("ocelot.json"); and app.UseOcelot().Wait(); (or UseOcelot with async). Ocelot forwards requests to the downstream host based on UpstreamPathTemplate.

How this fits together: Both Azure APIM and Ocelot give you routing and policies at the edge. You configure which path goes to which backend (BFF or service). Auth and rate limiting run at the gateway; aggregation runs in the BFF.


Implementation: BFF (ASP.NET Core)

A BFF is an ASP.NET Core API that calls backend services (via HTTP client or gRPC), aggregates results, and returns one DTO. It can handle failures (partial data, fallback, cache) so the client gets a useful response even when one service is down.

Step 1: Register HTTP clients for each backend

builder.Services.AddHttpClient<IOrderService, OrderServiceClient>(c =>
    c.BaseAddress = new Uri(builder.Configuration["Services:OrderService"]));
builder.Services.AddHttpClient<ICustomerService, CustomerServiceClient>(c =>
    c.BaseAddress = new Uri(builder.Configuration["Services:CustomerService"]));
builder.Services.AddHttpClient<IAnalyticsService, AnalyticsServiceClient>(c =>
    c.BaseAddress = new Uri(builder.Configuration["Services:AnalyticsService"]));

Step 2: Dashboard controller (aggregation + failure handling)

[ApiController]
[Route("api/[controller]")]
public class DashboardController : ControllerBase
{
    private readonly IOrderService _orders;
    private readonly ICustomerService _customers;
    private readonly IAnalyticsService _analytics;
    private readonly ILogger<DashboardController> _logger;

    public DashboardController(
        IOrderService orders,
        ICustomerService customers,
        IAnalyticsService analytics,
        ILogger<DashboardController> logger)
    {
        _orders = orders;
        _customers = customers;
        _analytics = analytics;
        _logger = logger;
    }

    [HttpGet]
    public async Task<ActionResult<DashboardDto>> GetDashboard(CancellationToken ct = default)
    {
        var ordersTask = _orders.GetRecentAsync(ct);
        var customersTask = _customers.GetTopAsync(ct);
        var analyticsTask = _analytics.GetSummaryAsync(ct);

        await Task.WhenAll(ordersTask.AsTask(), customersTask.AsTask(), analyticsTask.AsTask());

        var recentOrders = await ordersTask;
        var topCustomers = await customersTask;
        var analyticsSummary = await analyticsTask;

        if (recentOrders == null) _logger.LogWarning("Order service returned null; using empty list");
        if (topCustomers == null) _logger.LogWarning("Customer service returned null; using empty list");
        if (analyticsSummary == null) _logger.LogWarning("Analytics service returned null; using default");

        return Ok(new DashboardDto
        {
            RecentOrders = recentOrders ?? Array.Empty<OrderSummaryDto>(),
            TopCustomers = topCustomers ?? Array.Empty<CustomerSummaryDto>(),
            Analytics = analyticsSummary ?? new AnalyticsSummaryDto()
        });
    }
}

Step 3: Resilient HTTP client (optional)

Use Polly (retry, circuit breaker) on the HTTP clients so the BFF does not fail fast when one service is slow or down. Return partial data when a service fails (as above: null → empty or default).

How this fits together: The BFF is the only backend the web app calls for the dashboard. It calls Order, Customer, and Analytics services (in parallel), aggregates, and returns one DashboardDto. If one service fails, the BFF still returns a response (e.g. empty list for that part). The gateway in front of the BFF handles auth and rate limiting; the BFF handles aggregation and resilience.


Enterprise best practices

  1. Gateway for cross-cutting; BFF for aggregation. Do not put aggregation or business logic in the gateway. Gateway: route, auth, rate limit, cache, log. BFF: call services, aggregate, transform.
  2. One BFF per client type. Web BFF, Mobile BFF, Partner BFF. Do not share one BFF for all clients—each client type has different needs.
  3. BFF team = client team. The team that builds the frontend owns the BFF. They know what data the client needs and can change the BFF API without coordinating with every backend team.
  4. Gateway is infrastructure. Owned by platform/infra team. Same gateway can front multiple BFFs and services; policies (auth, rate limit) are consistent.
  5. Handle failures in BFF. When a backend service fails, return partial data (e.g. empty list), fallback (cached or default), or a degraded response. Do not fail the whole request if one service is down.
  6. Cache at both levels. Gateway caches full responses (e.g. by path + query). BFF can cache service responses (e.g. in-memory or Redis) to reduce load and improve latency.
  7. Monitor both. Track latency at the gateway (client → gateway) and errors in the BFF (service failures, timeouts). Use distributed tracing (e.g. OpenTelemetry) so a request is traced through gateway → BFF → services.
  8. Version BFF APIs. Clients depend on the BFF; version the BFF API (e.g. /api/v1/dashboard) so you can evolve without breaking clients.
  9. Auth at gateway. Validate tokens at the gateway; pass user context (e.g. user id, claims) to the BFF via headers or context. BFF and services trust the gateway for auth (or re-validate if needed).

Common issues and fixes

Below: frequent pitfalls and how to fix them so Gateway and BFF stay clearly split.

Issue Cause Fix
Gateway does too much Aggregation or business logic in gateway Move aggregation to BFF; keep gateway for routing and policies only.
Shared BFF One BFF for web, mobile, partner Split by client type: Web BFF, Mobile BFF, Partner BFF.
Chatty BFF Many sequential calls to backends Call services in parallel (Task.WhenAll); cache where appropriate; consider batch APIs on services.
No auth at gateway Auth implemented in each service or BFF only Centralize token validation at the gateway; pass user context downstream.
BFF couples to frontend BFF contains UI logic or too many frontend-specific details Keep BFF generic: aggregate and shape data; avoid hard-coding UI structure.
Latency Too many hops (client → gateway → BFF → services) Cache at gateway and BFF; parallel calls in BFF; optimize service APIs.
Single point of failure Gateway or BFF down = all clients affected High availability (multiple instances); health checks; circuit breaker in BFF for backends.
No partial response BFF fails entire request if one service fails Return partial data (null → empty/default); use Polly retry/circuit breaker; log and monitor.

Position & Rationale

I use a gateway at the edge for every multi-service system that exposes HTTP to clients—no exception. It is the single place for auth, rate limiting, and routing; pushing that into each service or BFF duplicates policy and makes change costly. I add a BFF only when a client type (web, mobile, partner) needs aggregated data from more than one service in one call; otherwise the gateway routing to services is enough. I avoid putting aggregation or business logic in the gateway: it scales and operates differently from an app service, and mixing concerns makes both harder to own. BFF-only without a gateway can work for small or internal systems, but as soon as you have more than one client type or need central rate limiting, a gateway pays off.


Trade-Offs & Failure Modes

  • What this sacrifices: An extra hop (client → gateway → BFF → services) and operational surface: two layers to deploy, monitor, and version. Latency and failure modes compound.
  • Where it degrades: When the BFF does too many sequential calls to backends, or when the gateway becomes a bottleneck (e.g. no caching, no horizontal scaling). When team boundaries are unclear, gateway vs BFF ownership drifts and duplication creeps in.
  • How it fails when misapplied: Using the gateway for aggregation (turning it into a “mega-BFF”) or using one BFF for all client types. Both create a monolith at the wrong layer. Failing the whole BFF response when one backend is down instead of returning partial data also misapplies the pattern.
  • Early warning signs: Gateway config growing with path-specific logic; BFF taking on auth or rate-limiting; frontend and backend teams arguing over who owns the BFF; no partial-response strategy when a backend fails.

What Most Guides Miss

Most docs describe Gateway and BFF in isolation and skip who owns what and what happens when a backend fails. In practice, the BFF must return something useful when one of N backends is slow or down—partial data, defaults, or cached—and that contract (and who defines it) needs to be explicit. Another gap: gateway policies (e.g. rate limit by user) often require a stable user identity; if the BFF or services don’t get that context from the gateway, you end up re-doing auth or passing tokens through. Deciding how user context flows (header, token forward, or re-validation) at the start avoids rework.


Decision Framework

  • If you have one client and one service per request → Gateway only; no BFF.
  • If you have one client type but need data from 2+ services for one screen → Add one BFF for that client; gateway routes to it.
  • If you have web + mobile + partner → One BFF per client type; gateway routes by path or host to the right BFF.
  • If latency is critical and you can’t add a hop → Consider BFF colocated with the client or a single aggregated backend; you give up central gateway policy.
  • If the BFF has more than a handful of backends → Question whether the BFF is doing too much; consider splitting by feature or screen.
  • If backends are inconsistent (timeouts, failures) → BFF must implement partial response, retries, and circuit breaker; don’t fail the whole request.

You can also explore more patterns in the API Architecture resource page.

Key Takeaways

  • Gateway at the edge for auth, rate limiting, routing; BFF behind it only when a client needs aggregation from multiple services.
  • One BFF per client type (web, mobile, partner); gateway does not aggregate or implement business logic.
  • BFF must return partial data when a backend fails; use Polly (retry, circuit breaker) and avoid failing the entire response.
  • Validate tokens at the gateway; pass user context to BFF and services so they don’t re-implement auth.
  • Ownership: platform owns gateway; frontend/client team owns BFF. Keep that boundary clear.

Summary

API Gateway is the single entry point at the edge (routing, auth, rate limiting, CORS, caching); BFF is a backend per client type that aggregates data from multiple services and shapes the response. Use Gateway alone when clients call one service per request; use Gateway + BFF when clients need data from many services in one call or different client types need different APIs. Putting aggregation in the gateway or skipping a BFF when clients need it leads to duplicated logic and unclear ownership; getting the split right gives you one place for cross-cutting policy and clear ownership (platform owns gateway, client team owns BFF). Next, map your client types and which backends each needs—then decide whether you need a BFF and who will own it.


When I Would Use This Again — and When I Wouldn’t

I would use Gateway + BFF again when: multiple client types (web, mobile, partner), multiple backends, and a need for both central policy (auth, rate limit) and client-specific aggregation. Same pattern fits greenfield microservices and gradual extraction from a monolith where the gateway becomes the single entry point.

I wouldn’t add a BFF when: clients only ever call one service per request, or when the system is a single service or two. I wouldn’t put aggregation in the gateway; if the team can’t own a separate BFF, I’d rather have a single “aggregation service” behind the gateway than overload the gateway. For internal-only or low-scale APIs, gateway-only (or even direct service access with a shared auth layer) may be enough; the full Gateway + BFF split pays off as client types and services grow.


If you’re building or modernizing API platforms, I provide API architecture consulting covering API-first design, governance, and integration strategy.

services
Frequently Asked Questions

Frequently Asked Questions

What is an API Gateway?

An API Gateway is a single entry point for client requests. It handles routing, auth, rate limiting, CORS, SSL termination, caching, and monitoring. It does not aggregate data or implement business logic.

What is a BFF?

BFF (Backend-for-Frontend) is a backend tailored to a specific client type (web, mobile, partner). It aggregates data from multiple services and shapes the response so the client gets one API call instead of many.

When should I use Gateway vs BFF?

Use Gateway for routing and cross-cutting concerns (auth, rate limit, CORS). Use BFF when clients need aggregated data from multiple services (e.g. dashboard) or different client types need different APIs.

Can I use both?

Yes. Many architectures use Gateway at the edge and BFF behind it. Gateway handles auth, rate limiting, and routing; BFF handles aggregation and transformation. Client → Gateway → BFF → Services.

Should I have one BFF or many?

One per client type. Web BFF for the SPA, Mobile BFF for the app, Partner BFF for third parties. Each serves its client’s specific needs.

Who owns the BFF?

The frontend team that builds the client. They own the BFF API so they can change it when the client needs change.

Who owns the Gateway?

The platform or infrastructure team. The gateway is shared infrastructure for all clients and backends.

Does the Gateway replace the BFF?

No. They serve different purposes. Gateway: cross-cutting (routing, auth, rate limit). BFF: aggregation and client-specific shaping.

What if the BFF becomes too complex?

Split into smaller BFFs (e.g. by feature or screen) or consider GraphQL (single endpoint, client-defined queries). Keep each BFF focused on its client.

Can the BFF be the Gateway?

Technically yes (one app that routes and aggregates), but mixing concerns makes it harder to scale, secure, and own. Better to separate: gateway for edge, BFF for aggregation.

What is Azure API Management?

Azure API Management is Azure’s managed API Gateway. It provides routing, auth (e.g. JWT validation), rate limiting, caching, and policies. You define APIs, backends, and policies in the portal or API.

What is Ocelot?

Ocelot is a .NET API Gateway library. You run it as an ASP.NET Core app and configure routes and downstream services (JSON or code). Use it when you self-host or run in Kubernetes and do not use Azure APIM.

Should I use GraphQL instead of BFF?

GraphQL can reduce the need for a BFF if clients can define their own queries and the backend supports GraphQL. You still need aggregation somewhere (e.g. GraphQL resolvers calling services). BFF and GraphQL can coexist: BFF could expose a GraphQL endpoint that aggregates from REST services.

How do I handle auth?

Validate tokens at the Gateway. After validation, pass user context (e.g. user id, claims) to the BFF and services via headers (e.g. X-User-Id) or by forwarding the token. BFF and services trust the gateway or re-validate as needed.

How do I monitor Gateway and BFF?

Use Application Insights, OpenTelemetry, or similar. Track latency (client → gateway, gateway → BFF, BFF → services), errors (4xx, 5xx), and throughput. Use distributed tracing so one request is traced across gateway, BFF, and services.

What if a service fails in the BFF?

Return partial data (e.g. empty list or default for that part of the response). Use Polly (retry, circuit breaker) so transient failures are retried and persistent failures do not bring down the BFF. Log and monitor so you can fix the backend. Do not fail the whole request for one service failure.

services
Related Guides & Resources

services
Related services