BFF Versioning

BFF Versioning

BFF versioning is the strategy for managing breaking changes to the BFF-Pattern contract between the backend-for-frontend and its frontend client. Because the BFF owns the API contract for exactly one frontend, versioning is simpler than versioning a public API — but it still requires a deliberate strategy to support zero-downtime deployments.

Why BFF Versioning Is Different

A traditional API serves many unknown consumers; versioning is defensive and long-lived. A BFF serves one frontend team; versioning is coordinated and can be short-lived. The frontend team and the BFF team are often the same team, so the "contract" is an internal coordination mechanism rather than a public commitment.

That said, versioning still matters for:

  • Blue/green deployments — old Angular build and new Angular build may coexist briefly
  • Canary releases — routing a percentage of users to a new Angular build that expects a new BFF API shape
  • Mobile apps — if a BFF also serves a mobile client, mobile release cycles are slower; old versions stay in the field longer

Three Versioning Strategies

StrategyURL ExampleProsCons
URL path versioning/api/v1/products, /api/v2/productsSimple, cacheable, easy to route, visible in logsURL "pollution"; Angular service class must embed version in base URL
Header versioningAccept: application/vnd.myapp.v2+jsonClean URLs; version is metadata, not resource identityHarder to test in browser; less visible; caching requires Vary header
Query parameter/api/products?version=2Easy to add without route changesPollutes query strings; harder to cache; easy to omit by mistake

Recommendation for BFF Serving Angular

URL path versioning (/api/v1/, /api/v2/) is the pragmatic choice. Reasons:

  1. Angular's HttpClient service can be given a base URL constant (/api/v2) so the version is defined in one place
  2. Routes are trivially distinguishable in Spring-Cloud-Gateway predicates and access logs
  3. During deployment, old and new routes can coexist on the same BFF instance
  4. Testing is trivial — curl /api/v2/products works without setting custom headers

Versioning in Spring Cloud Gateway

Spring-Cloud-Gateway makes it straightforward to maintain two versions simultaneously with RewritePath filters:

spring:
  cloud:
    gateway:
      routes:
        - id: products-v2
          uri: lb://products-service
          predicates:
            - Path=/api/v2/products/**
          filters:
            - RewritePath=/api/v2/products/(?<segment>.*), /products/${segment}
 
        - id: products-v1-compat
          uri: lb://products-service
          predicates:
            - Path=/api/v1/products/**
          filters:
            - RewritePath=/api/v1/products/(?<segment>.*), /products/${segment}
            # ResponseTransformer filter adapts v2 response shape to v1 contract
            - name: ResponseTransformer

The products-service itself only needs to maintain one internal API; the BFF handles shape adaptation via the ResponseTransformer filter for the v1 compatibility layer.

Lifecycle of a BFF Version

v1 deployed (Angular build A)
   ↓
v2 deployed alongside v1 (Angular build B canary)
   ↓
Traffic shifts to build B → all users on v2
   ↓
v1 routes removed (after N days/weeks)

The rule for BFF is to maintain N and N-1 simultaneously during deployment. Once 100% of traffic is on the new Angular build, the old version can be removed from gateway configuration.

Angular Side: Isolating the Version

Define the API version once in Angular so changing it is a single-line update:

// environment.ts
export const environment = {
  apiBase: '/api/v2'
};
 
// product.service.ts
@Injectable({ providedIn: 'root' })
export class ProductService {
  private readonly base = `${environment.apiBase}/products`;
 
  constructor(private http: HttpClient) {}
 
  getProducts(params: ProductQueryParams): Observable<PageResponse<ProductView>> {
    return this.http.get<PageResponse<ProductView>>(this.base, {
      params: toHttpParams(params)
    });
  }
}

Version Negotiation via Gateway Rewriting

For cases where the Angular client and the downstream service have fallen out of sync on response shape, the BFF can act as an adapter:

// GatewayFilter that transforms a v2 response into v1 shape for backwards compat
@Component
public class V1ResponseAdapterFilter implements GatewayFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange.mutate()
            .response(new V1ResponseAdapter(exchange.getResponse()))
            .build());
    }
}

This keeps the downstream service at its current shape while the BFF provides the translation layer — consistent with the Response-Transformation pattern.


  • REST-API-Design — BFF-Versioning covers gateway-level and frontend-environment versioning strategies; REST-API-Design covers public API versioning lifecycle (Sunset/Deprecation headers) — they are complementary notes, explicitly cross-linked