BFF Phase 6: Angular-Specific BFF Design
BFF Phase 6: Angular-Specific BFF Design
This note covers the Angular-specific design considerations for a Backend-for-Frontend built on Spring Boot 3.4.x / Spring Cloud Gateway. It synthesises phases 1–5 through the lens of what Angular components actually need from a BFF, and closes the research project with deployment and versioning patterns.
Related research: P1-BFF-Foundations | P2-Spring-Boot-BFF-Stack | P3-BFF-Implementation-Patterns | P4-BFF-Security | P5-BFF-Observability-Testing
ANG-01: Component-Driven API Design
The fundamental BFF design principle for Angular: identify what each Angular component needs, then design the BFF endpoint to deliver exactly that — do not expose microservice structure to the frontend.
The Wrong Approach
Angular ProductListComponent
→ GET /api/products (product service)
→ GET /api/categories (category service)
→ GET /api/promotions/active (promotions service)
[3 sequential or parallel HTTP calls, client-side assembly]
The BFF Approach
Angular ProductListComponent
→ GET /api/products-page (BFF endpoint)
[1 call, BFF assembles the composite response]
The BFF endpoint GET /api/products-page internally fans out to product, category, and promotions services using Mono.zip, assembles a ProductListPageResponse, and returns it. The Angular component has a single data contract and a single loading state to manage.
Composite DTO Pattern
Design DTOs around the Angular view, not around microservice boundaries:
// What the Angular dashboard page needs in a single call
public record DashboardPageResponse(
UserSummaryDto user,
List<OrderSummaryDto> recentOrders,
AccountBalanceDto balance,
List<NotificationDto> unreadNotifications
) {}
@GetMapping("/api/dashboard")
public Mono<DashboardPageResponse> getDashboard(
@AuthenticationPrincipal Mono<OidcUser> principal) {
return principal.flatMap(user -> {
String userId = user.getSubject();
return Mono.zip(
userServiceClient.getSummary(userId),
orderServiceClient.getRecent(userId, 5),
accountServiceClient.getBalance(userId),
notificationServiceClient.getUnread(userId)
).map(tuple -> new DashboardPageResponse(
tuple.getT1(),
tuple.getT2(),
tuple.getT3(),
tuple.getT4()
));
});
}Standardised Pagination Envelope
Angular's table and list components need a consistent pagination contract. The BFF translates Spring Data Page<T> from downstream services into a client-friendly envelope:
public record PageResponse<T>(
List<T> content,
int page,
int size,
long totalElements,
int totalPages,
boolean first,
boolean last
) {}This PageResponse<T> is the single pagination contract across all BFF endpoints. Angular components depend only on this shape — not on Spring Data internals.
ANG-02: Pagination, Sorting, and Filtering
Angular's HttpClient sends query parameters that the BFF validates, sanitises, and translates to downstream service calls.
BFF Endpoint
@GetMapping("/api/products")
public Mono<PageResponse<ProductView>> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String sort,
@RequestParam(required = false) String search) {
// Guard: cap page size to prevent accidental or malicious large requests
int safeSize = Math.min(size, 100);
// Guard: validate sort field against allowlist (prevent injection)
String safeSort = validateSortField(sort);
return productServiceClient.getProducts(page, safeSize, safeSort, search)
.map(this::toPageResponse);
}
private String validateSortField(String sort) {
if (sort == null) return null;
Set<String> allowed = Set.of("name,asc", "name,desc", "price,asc", "price,desc", "createdAt,desc");
return allowed.contains(sort) ? sort : "name,asc"; // default on invalid
}
private PageResponse<ProductView> toPageResponse(Page<ProductDto> page) {
List<ProductView> views = page.getContent().stream()
.map(this::toView)
.toList();
return new PageResponse<>(
views,
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.getTotalPages(),
page.isFirst(),
page.isLast()
);
}Angular Side
export interface ProductQueryParams {
page?: number;
size?: number;
sort?: string;
search?: string;
}
@Injectable({ providedIn: 'root' })
export class ProductService {
constructor(private http: HttpClient) {}
getProducts(params: ProductQueryParams): Observable<PageResponse<ProductView>> {
let httpParams = new HttpParams();
if (params.page !== undefined) httpParams = httpParams.set('page', params.page);
if (params.size !== undefined) httpParams = httpParams.set('size', params.size);
if (params.sort) httpParams = httpParams.set('sort', params.sort);
if (params.search) httpParams = httpParams.set('search', params.search);
return this.http.get<PageResponse<ProductView>>('/api/products', { params: httpParams });
}
}Key BFF responsibilities:
- Cap page size — protect downstream from
size=10000requests - Validate sort fields — allowlist prevents sort-based injection attacks
- Translate Angular's
sort=name,ascparam format to whatever the downstream service expects (may differ) - Normalise the response — always return
PageResponse<T>regardless of what the downstream service returns
ANG-03: Real-Time Data — SSE vs WebSocket
See Server-Sent-Events for the full comparison and code reference. Summary:
Use SSE when: notifications, live feed updates, progress events, status polling replacement — any server-initiated, unidirectional data push.
Use WebSocket when: chat, collaborative editing, gaming, or any bidirectional communication where the client also sends messages in real time.
For the typical Angular SPA served by a BFF, SSE is the correct choice in 80%+ of real-time use cases. The BFF exposes a Flux<ServerSentEvent<T>> endpoint; Angular consumes it with EventSource + withCredentials: true.
Spring Boot SSE Endpoint
@GetMapping(
value = "/api/notifications/stream",
produces = MediaType.TEXT_EVENT_STREAM_VALUE
)
public Flux<ServerSentEvent<NotificationDto>> streamNotifications(
@AuthenticationPrincipal Mono<OidcUser> user) {
return user.flatMapMany(u ->
notificationService.getNotificationsForUser(u.getSubject())
.map(notification -> ServerSentEvent.<NotificationDto>builder()
.id(notification.id())
.event("notification")
.data(notification)
.build())
);
}The @AuthenticationPrincipal Mono<OidcUser> injection means authentication is handled by the BFF's existing OAuth2 session — the SSE endpoint inherits the same security context as all other BFF endpoints.
Angular EventSource Consumer
const eventSource = new EventSource('/api/notifications/stream', {
withCredentials: true // essential: sends the session cookie
});
eventSource.addEventListener('notification', (event: MessageEvent) => {
const notification = JSON.parse(event.data) as NotificationDto;
this.notificationSubject.next(notification);
});ANG-04: BFF as Single CORS Origin
In production, the Angular SPA and the BFF share the same HTTP origin. This is the cleanest security posture:
- Angular static files served from CDN at
app.example.com - All
/api/*requests routed to BFF at the sameapp.example.comhostname - The browser never makes a cross-origin request — CORS is irrelevant
For development, Angular's proxy configuration forwards API calls to the locally running BFF:
// proxy.conf.json (Angular)
{
"/api": {
"target": "http://localhost:8080",
"secure": false,
"changeOrigin": true
}
}// angular.json
{
"serve": {
"options": {
"proxyConfig": "proxy.conf.json"
}
}
}The BFF's development CORS configuration (see P4-BFF-Security for full detail):
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:4200"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true); // required for session cookies
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}allowCredentials(true) is required because Angular sends the session cookie. This also means allowedOrigins cannot use the wildcard * — an explicit origin is required.
In production (same origin), this CORS configuration is not needed. Keep a profile guard so it only activates in development.
ANG-05: BFF API Versioning Strategy
See BFF-Versioning for the full comparison table and Spring Cloud Gateway YAML. Summary:
URL path versioning (/api/v1/, /api/v2/) is the pragmatic choice for a BFF serving Angular:
- Angular services embed the version constant once (
environment.apiBase = '/api/v2') - Spring-Cloud-Gateway routes distinguish versions via
Pathpredicates - The BFF maintains N and N-1 during deployment windows
- Old v1 routes can use a
ResponseTransformerfilter to adapt v2 response shapes for backward compatibility
The BFF version tracks the Angular application version — when the Angular app ships a breaking data contract change, the BFF gets a new version prefix and the old prefix is maintained until all users have updated.
ANG-06: Deployment Patterns
See BFF-Deployment-Patterns for full topology diagrams and trade-offs. Summary:
Topology 1 (Standalone BFF) — baseline. BFF behind a load balancer, Redis for session, downstream services on private network.
Topology 2 (BFF at Edge / CDN) — production recommendation. CDN serves Angular static assets; /api/* routes to BFF on the same origin. Eliminates CORS entirely.
Topology 3 (API Gateway + BFF layered) — enterprise. Infrastructure API Gateway handles TLS, rate limiting, DDoS; BFF handles Angular-specific aggregation and session.
Key principle: downstream services are never accessible directly from the internet. The BFF is the single ingress point for the Angular application.
ANG-07: When to Split BFFs
Split rule: one BFF per frontend team / frontend application.
Split when:
- Mobile app needs different data shapes (e.g., deeply nested for REST vs flat for GraphQL-style mobile clients)
- Partner API uses different authentication (API key vs OAuth2 PKCE)
- Client types have different SLAs (internal dashboard vs customer-facing, where downtime tolerance differs)
- Release cadences diverge significantly (mobile app releases quarterly; web app releases weekly)
Do not split when:
- It is simply different pages/features of the same Angular app — use route prefixes in one BFF
- The team cannot sustain multiple deployments — operational overhead is real
Multi-BFF monorepo layout:
bff-web/ ← Angular web app BFF (OAuth2 + session cookies)
bff-mobile/ ← React Native BFF (JWT, stateless)
bff-partner/ ← Partner API BFF (API key auth, dedicated rate limits)
shared-lib/ ← Common: PageResponse<T>, circuit breaker config, tracing setup
Synthesis: The BFF Architecture Decision Framework
After six phases of research, the BFF pattern resolves to a clear decision framework:
| Question | If Yes | If No |
|---|---|---|
| Does the frontend need data aggregated from 3+ services per page? | Strong BFF case | Consider simpler proxy or direct client calls |
| Is there a team dedicated to the frontend? | BFF enables team autonomy | Shared backend may reduce overhead |
| Is the frontend a SPA (Angular / React)? | BFF-for-SPA security required | Evaluate other auth patterns |
| Does the app need real-time features (live updates)? | SSE via BFF is natural | Standard REST BFF sufficient |
| Are there multiple distinct client types (web + mobile + partner)? | One BFF per client type | Single shared BFF |
The 5 Most Important Insights
-
BFF is an organisational pattern first, a technical pattern second — team autonomy and the ability for a frontend team to evolve its API contract without coordinating with every downstream team is the core value proposition. The technical implementation follows from that.
-
BFF and API Gateway are not competitors — they solve different problems at different layers. The API Gateway is infrastructure (TLS, DDoS, rate limiting, API key auth). The BFF is an application-level aggregation and adaptation layer. Both can and often should coexist (Topology 3).
-
For SPAs: httpOnly session cookie in BFF is non-negotiable — storing OAuth2 tokens in
localStorageorsessionStorageis a critical XSS vulnerability. The BFF session pattern (httpOnly cookie → BFF holds token → BFF relays token to downstream) is the only correct security model for an Angular SPA. See OAuth2-BFF-Pattern and BFF-For-SPA. -
Reactive (WebFlux) is not optional for BFF — a BFF that fans out to 3–5 services per request must be non-blocking. A blocking servlet stack holds a thread per in-flight downstream call; under load, fan-out amplifies thread exhaustion. Project-Reactor
Mono.zip+ WebFlux is architecturally required for aggregation at scale. -
Consumer-driven contracts close the stub-drift loop — WireMock stubs that are not verified against the real service will drift and mask integration bugs. Consumer-Driven-Contract-Testing with Spring Cloud Contract: the BFF team writes the contracts, downstream teams run the verifier. When a downstream service breaks the contract, the build fails before the BFF is deployed.
The 3 Most Common Mistakes
-
Turning BFF into a Smart Gateway — avoid putting business logic that belongs in domain services into the BFF. The BFF aggregates and transforms; it does not enforce business rules, calculate prices, or manage inventory. When the BFF starts containing domain logic, it becomes a distributed monolith.
-
Disabling CSRF on a browser-facing BFF — a BFF using session cookies for authentication is subject to Cross-Site Request Forgery. CSRF protection must stay enabled. The Spring Security
CsrfWebFilteris disabled by many developers because it complicates initial setup. Re-enabling it is non-negotiable. See P4-BFF-Security. -
Choosing the servlet stack (Spring MVC) for a BFF — Spring MVC can technically build a BFF, but each downstream call blocks a thread from the servlet pool. With 3 downstream calls per request, the effective throughput is cut to one-third. Spring WebFlux with
Mono.ziphandles all three calls on a single event loop thread. The Reactive-Programming stack is architecturally required.
Open Questions for Future Research
- gRPC streaming from BFF — can the BFF expose
Flux-based SSE to Angular while internally consuming gRPC server-streaming from downstream services? What is the mapping between gRPC flow control and reactive back-pressure? - GraphQL Federation as BFF alternative — a federated GraphQL gateway (Apollo Federation, Netflix DGS) provides some of the same aggregation benefits as a BFF. When is GraphQL Federation preferable? When does it add complexity without benefit?
- BFF in Kubernetes with service mesh — with Istio or Linkerd providing mTLS, circuit breaking, and observability at the mesh level, what does the BFF still own? Which responsibilities overlap? Which remain BFF-specific?
- BFF cold start in GraalVM native — Spring Boot 3 supports GraalVM native images. What is the trade-off for BFF (instant startup vs longer build, missing reflection-heavy libraries)?