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.


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: 3s

application-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: DEBUG

Core 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

PhaseNoteFocus
P1P1-BFF-FoundationsPattern origin, Sam Newman, BFF vs API Gateway, architecture trade-offs
P2P2-Spring-Boot-BFF-StackSpring Cloud Gateway, WebFlux reactive stack, Mono/Flux, dependency matrix
P3P3-BFF-Implementation-PatternsAggregation with Mono.zip, response transformation, circuit breaker, error normalisation
P4P4-BFF-SecurityOAuth2 PKCE, token relay, BFF-for-SPA session pattern, CORS, CSRF, rate limiting
P5P5-BFF-Observability-TestingMicrometer tracing, Prometheus, structured logging, WireMock, contract testing
P6P6-BFF-Angular-IntegrationComponent-driven API design, pagination, SSE, CORS single origin, versioning, deployment

Topic Notes

TopicOne-Line Summary
BFF-PatternThe core pattern: one backend per frontend, owned by the frontend team
API-Gateway-PatternInfrastructure-level gateway; BFF is above it, not competing with it
Sam-NewmanOriginator of the BFF pattern (2015 microservices-demo blog post)
Spring-Cloud-GatewayReactive Spring gateway that serves as the BFF runtime
Project-ReactorMono/Flux reactive types powering non-blocking BFF aggregation
Reactive-ProgrammingNon-blocking I/O model required for fan-out aggregation
Request-AggregationParallel downstream calls using Mono.zip
Response-TransformationShaping downstream responses into Angular-friendly DTOs
Circuit-Breaker-PatternProtecting the BFF from downstream failures
Resilience4jSpring Boot 3.x circuit breaker implementation
OAuth2-BFF-PatternOAuth2 with BFF as confidential client holding tokens server-side
Token-Relay-PatternBFF forwarding access tokens to downstream services via TokenRelay filter
BFF-For-SPAhttpOnly session cookie pattern — the only correct SPA auth model
PKCEProof Key for Code Exchange — required for BFF acting as public client
Distributed-TracingEnd-to-end trace propagation across BFF and downstream services
MicrometerSpring Boot 3.x metrics and tracing facade (replaces Spring Cloud Sleuth)
Consumer-Driven-Contract-TestingBFF writes contracts; downstream services verify — closes stub-drift loop
WireMockHTTP stubbing for BFF integration tests against downstream services
Server-Sent-EventsReactive SSE from BFF to Angular for real-time notifications
BFF-VersioningURL path versioning strategy for Angular BFF API evolution
BFF-Deployment-PatternsStandalone, 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.