BFF Spring Boot Starter Architecture
BFF Spring Boot Starter Architecture
Reference architecture for a production-ready Backend-for-Frontend serving an Angular 17+ SPA, built on Spring Boot 3.4.x and Spring Cloud Gateway 2024.0.x.
This project note consolidates all six phases of the BFF Architecture research into an actionable starting point for a new BFF service.
See the full research index at BFF-Architecture-MOC.
Recommended Project Structure
bff-service/
├── src/main/java/com/example/bff/
│ ├── BffApplication.java
│ ├── config/
│ │ ├── SecurityConfig.java ← OAuth2 + CSRF + CORS
│ │ ├── GatewayConfig.java ← Programmatic routes (if needed)
│ │ └── ObservabilityConfig.java ← Micrometer customisation
│ ├── filter/
│ │ ├── CorrelationIdFilter.java ← GlobalFilter, Ordered(-1)
│ │ └── RequestLoggingFilter.java
│ ├── aggregation/
│ │ └── DashboardAggregationService.java ← Mono.zip fan-out
│ └── dto/
│ ├── DashboardResponse.java
│ └── PageResponse.java
├── src/main/resources/
│ ├── application.yml ← Routes, actuator, session
│ └── application-local.yml ← Dev overrides (localhost IdP)
└── src/test/
├── integration/
│ └── DashboardIntegrationTest.java ← @WireMockTest
└── contract/
└── contracts/ ← Spring Cloud Contract Groovy DSL
Gradle Dependencies (Kotlin DSL)
// build.gradle.kts
plugins {
id("org.springframework.boot") version "3.4.3"
id("io.spring.dependency-management") version "1.1.7"
id("org.springframework.cloud.contract") version "4.2.0"
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
}
extra["springCloudVersion"] = "2024.0.1"
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
}
}
dependencies {
// Core BFF stack
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.cloud:spring-cloud-starter-gateway")
// Security: OAuth2 Client + session-based token storage
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
// Session: Redis-backed httpOnly cookie session
implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive")
implementation("org.springframework.session:spring-session-data-redis")
// Resilience: circuit breakers on all outbound calls
implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j")
// Observability: Micrometer Tracing + OTLP + Prometheus
implementation("io.micrometer:micrometer-tracing-bridge-otel")
implementation("io.opentelemetry:opentelemetry-exporter-zipkin")
implementation("io.micrometer:micrometer-registry-prometheus")
// Test: WireMock, contract stubs, reactor test
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
testImplementation("com.github.tomakehurst:wiremock-spring-boot:3.4.0")
testImplementation("io.projectreactor:reactor-test")
}Key Configuration Files
application.yml (annotated skeleton)
spring:
application:
name: bff-service
# Redis-backed session (httpOnly cookie)
session:
store-type: redis
redis:
flush-mode: on-save
namespace: bff:session
timeout: 3600s # match OAuth2 token TTL
data:
redis:
host: ${REDIS_HOST:localhost}
port: 6379
# OAuth2 Client
security:
oauth2:
client:
registration:
keycloak:
client-id: ${OAUTH2_CLIENT_ID:bff-client}
client-secret: ${OAUTH2_CLIENT_SECRET}
scope: [openid, profile, email]
authorization-grant-type: authorization_code
redirect-uri: "${OAUTH2_REDIRECT_URI:{baseUrl}/login/oauth2/code/{registrationId}}"
provider:
keycloak:
issuer-uri: ${OAUTH2_ISSUER_URI:http://localhost:9090/realms/dev}
# Spring Cloud Gateway routes
cloud:
gateway:
routes:
- id: products-v2
uri: lb://products-service
predicates:
- Path=/api/v2/products/**
filters:
- RewritePath=/api/v2/products/(?<segment>.*), /products/${segment}
- TokenRelay=
- name: CircuitBreaker
args:
name: products-cb
fallbackUri: forward:/api/fallback/products
- id: orders-v2
uri: lb://orders-service
predicates:
- Path=/api/v2/orders/**
filters:
- RewritePath=/api/v2/orders/(?<segment>.*), /orders/${segment}
- TokenRelay=
- name: CircuitBreaker
args:
name: orders-cb
fallbackUri: forward:/api/fallback/orders
# Actuator: expose health + prometheus
management:
endpoints:
web:
exposure:
include: health, info, prometheus, metrics
endpoint:
health:
show-details: always
tracing:
sampling:
probability: 1.0 # 100% sampling; reduce in production
metrics:
distribution:
percentiles-histogram:
http.server.requests: true
# Resilience4j circuit breaker defaults
resilience4j:
circuitbreaker:
instances:
products-cb:
sliding-window-size: 10
failure-rate-threshold: 50
wait-duration-in-open-state: 10s
orders-cb:
sliding-window-size: 10
failure-rate-threshold: 50
wait-duration-in-open-state: 10s
timelimiter:
instances:
products-cb:
timeout-duration: 3s
orders-cb:
timeout-duration: 3sapplication-local.yml (development overrides)
spring:
session:
store-type: none # in-memory session for local dev (no Redis needed)
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: http://localhost:9090/realms/dev
logging:
level:
org.springframework.cloud.gateway: DEBUG
org.springframework.security: DEBUGCore Java Classes
SecurityConfig.java
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange(auth -> auth
.pathMatchers("/actuator/health", "/actuator/info").permitAll()
.pathMatchers("/api/auth/**").permitAll()
.anyExchange().authenticated()
)
.oauth2Login(Customizer.withDefaults())
.csrf(csrf -> csrf
.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new SpaServerCsrfTokenRequestHandler())
)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.build();
}
@Bean
@Profile("local")
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);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}CorrelationIdFilter.java
@Component
@Order(-1)
public class CorrelationIdFilter implements GlobalFilter {
private static final String CORRELATION_ID_HEADER = "X-Correlation-ID";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String correlationId = exchange.getRequest().getHeaders()
.getFirst(CORRELATION_ID_HEADER);
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
}
final String id = correlationId;
ServerWebExchange mutated = exchange.mutate()
.request(r -> r.header(CORRELATION_ID_HEADER, id))
.response(r -> { r.getHeaders().add(CORRELATION_ID_HEADER, id); return r; })
.build();
return chain.filter(mutated)
.contextWrite(Context.of("correlationId", id));
}
}DashboardAggregationService.java
@Service
public class DashboardAggregationService {
private final UserServiceClient userClient;
private final OrderServiceClient orderClient;
private final AccountServiceClient accountClient;
private final NotificationServiceClient notificationClient;
// constructor injection...
public Mono<DashboardPageResponse> getDashboard(String userId) {
return Mono.zip(
userClient.getSummary(userId),
orderClient.getRecent(userId, 5),
accountClient.getBalance(userId),
notificationClient.getUnread(userId)
).map(tuple -> new DashboardPageResponse(
tuple.getT1(),
tuple.getT2(),
tuple.getT3(),
tuple.getT4()
));
}
}PageResponse.java
public record PageResponse<T>(
List<T> content,
int page,
int size,
long totalElements,
int totalPages,
boolean first,
boolean last
) {
public static <T> PageResponse<T> from(org.springframework.data.domain.Page<T> page) {
return new PageResponse<>(
page.getContent(),
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.getTotalPages(),
page.isFirst(),
page.isLast()
);
}
}Research and Topic Notes Reference
Research Phases
| Phase | Note | Focus |
|---|---|---|
| P1 | P1-BFF-Foundations | Pattern origin, Sam Newman, BFF vs API Gateway, architecture trade-offs |
| P2 | P2-Spring-Boot-BFF-Stack | Spring Cloud Gateway, WebFlux reactive stack, Mono/Flux, dependency matrix |
| P3 | P3-BFF-Implementation-Patterns | Aggregation with Mono.zip, response transformation, circuit breaker, error normalisation |
| P4 | P4-BFF-Security | OAuth2 PKCE, token relay, BFF-for-SPA session pattern, CORS, CSRF, rate limiting |
| P5 | P5-BFF-Observability-Testing | Micrometer tracing, Prometheus, structured logging, WireMock, contract testing |
| P6 | P6-BFF-Angular-Integration | Component-driven API design, pagination, SSE, CORS single origin, versioning, deployment |
Topic Notes
| Topic | One-Line Summary |
|---|---|
| BFF-Pattern | The core pattern: one backend per frontend, owned by the frontend team |
| API-Gateway-Pattern | Infrastructure-level gateway; BFF is above it, not competing with it |
| Sam-Newman | Originator of the BFF pattern (2015 microservices-demo blog post) |
| Spring-Cloud-Gateway | Reactive Spring gateway that serves as the BFF runtime |
| Project-Reactor | Mono/Flux reactive types powering non-blocking BFF aggregation |
| Reactive-Programming | Non-blocking I/O model required for fan-out aggregation |
| Request-Aggregation | Parallel downstream calls using Mono.zip |
| Response-Transformation | Shaping downstream responses into Angular-friendly DTOs |
| Circuit-Breaker-Pattern | Protecting the BFF from downstream failures |
| Resilience4j | Spring Boot 3.x circuit breaker implementation |
| OAuth2-BFF-Pattern | OAuth2 with BFF as confidential client holding tokens server-side |
| Token-Relay-Pattern | BFF forwarding access tokens to downstream services via TokenRelay filter |
| BFF-For-SPA | httpOnly session cookie pattern — the only correct SPA auth model |
| PKCE | Proof Key for Code Exchange — required for BFF acting as public client |
| Distributed-Tracing | End-to-end trace propagation across BFF and downstream services |
| Micrometer | Spring Boot 3.x metrics and tracing facade (replaces Spring Cloud Sleuth) |
| Consumer-Driven-Contract-Testing | BFF writes contracts; downstream services verify — closes stub-drift loop |
| WireMock | HTTP stubbing for BFF integration tests against downstream services |
| Server-Sent-Events | Reactive SSE from BFF to Angular for real-time notifications |
| BFF-Versioning | URL path versioning strategy for Angular BFF API evolution |
| BFF-Deployment-Patterns | Standalone, CDN-at-edge, API-Gateway-layered topology patterns |
Synthesis
BFF-Final-Synthesis — capstone note with decision framework, 5 key insights, 3 common mistakes, and open research questions.