Spring Boot Advanced Architecture Interview Guide: Senior-Level Deep Dive

Sławomir Plamowski 14 min read
architecture interview-preparation java microservices senior spring-boot spring-cloud

Senior Java developer interviews go beyond basic Spring Boot usage. Interviewers expect you to understand how Spring Boot works under the hood, architect production-ready systems, and make informed decisions about reactive vs imperative programming, microservices patterns, and performance optimization.

This guide covers the advanced Spring Boot topics that distinguish senior engineers: internals, custom starters, Spring Cloud, WebFlux, and production architecture patterns.

1. Spring Boot Internals

Understanding how Spring Boot works enables better debugging and custom solutions.

Auto-Configuration Mechanism

How does Spring Boot auto-configuration actually work?

Auto-Configuration Flow:
┌──────────────────────────────────────────────────────────────┐
│  @SpringBootApplication                                       │
│  └── @EnableAutoConfiguration                                 │
│       └── @Import(AutoConfigurationImportSelector.class)      │
└──────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌──────────────────────────────────────────────────────────────┐
│  AutoConfigurationImportSelector                              │
│  1. Load META-INF/spring/...AutoConfiguration.imports         │
│  2. Filter by @Conditional annotations                        │
│  3. Order by @AutoConfigureOrder, Before, After               │
│  4. Return matching configuration classes                     │
└──────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌──────────────────────────────────────────────────────────────┐
│  Conditional Evaluation                                       │
│  @ConditionalOnClass - Class exists on classpath?             │
│  @ConditionalOnMissingBean - Bean not already defined?        │
│  @ConditionalOnProperty - Property set to expected value?     │
│  @ConditionalOnWebApplication - Web app context?              │
└──────────────────────────────────────────────────────────────┘

Examining an actual auto-configuration class:

// DataSourceAutoConfiguration (simplified)
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {

    @Configuration(proxyBeanMethods = false)
    @Conditional(EmbeddedDatabaseCondition.class)
    @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
    @Import(EmbeddedDataSourceConfiguration.class)
    protected static class EmbeddedDatabaseConfiguration {
    }

    @Configuration(proxyBeanMethods = false)
    @Conditional(PooledDataSourceCondition.class)
    @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
    @Import({ DataSourceConfiguration.Hikari.class,
              DataSourceConfiguration.Tomcat.class,
              DataSourceConfiguration.Dbcp2.class })
    protected static class PooledDataSourceConfiguration {
    }
}

Key insight: Auto-configuration provides defaults that back off when you define your own beans.

@Conditional Annotations Deep Dive

// Built-in conditions
@ConditionalOnClass(DataSource.class)           // Class on classpath
@ConditionalOnMissingClass("com.example.Foo")   // Class NOT on classpath
@ConditionalOnBean(DataSource.class)            // Bean exists
@ConditionalOnMissingBean(DataSource.class)     // Bean doesn't exist
@ConditionalOnProperty(                         // Property matches
    prefix = "app.feature",
    name = "enabled",
    havingValue = "true",
    matchIfMissing = false
)
@ConditionalOnResource(resources = "classpath:schema.sql")
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnExpression("${app.advanced:false} and ${app.experimental:false}")

// Custom condition
public class OnProductionEnvironmentCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context,
                          AnnotatedTypeMetadata metadata) {
        Environment env = context.getEnvironment();
        String[] activeProfiles = env.getActiveProfiles();
        return Arrays.asList(activeProfiles).contains("production");
    }
}

@Configuration
@Conditional(OnProductionEnvironmentCondition.class)
public class ProductionOnlyConfiguration {
    // Only loaded in production
}

Bean Lifecycle

What is the complete Spring bean lifecycle?

Bean Lifecycle Phases:
┌─────────────────────────────────────────────────────────────┐
│ 1. Instantiation                                             │
│    - Constructor called                                      │
│    - Dependencies injected (constructor injection)           │
├─────────────────────────────────────────────────────────────┤
│ 2. Population                                                │
│    - @Autowired fields/setters injected                      │
│    - @Value properties resolved                              │
├─────────────────────────────────────────────────────────────┤
│ 3. Aware Interfaces                                          │
│    - BeanNameAware.setBeanName()                             │
│    - BeanFactoryAware.setBeanFactory()                       │
│    - ApplicationContextAware.setApplicationContext()         │
├─────────────────────────────────────────────────────────────┤
│ 4. Pre-Initialization                                        │
│    - BeanPostProcessor.postProcessBeforeInitialization()     │
│    - @PostConstruct methods                                  │
├─────────────────────────────────────────────────────────────┤
│ 5. Initialization                                            │
│    - InitializingBean.afterPropertiesSet()                   │
│    - Custom init-method                                      │
├─────────────────────────────────────────────────────────────┤
│ 6. Post-Initialization                                       │
│    - BeanPostProcessor.postProcessAfterInitialization()      │
│    - AOP proxies created here                                │
├─────────────────────────────────────────────────────────────┤
│ 7. Ready for Use                                             │
├─────────────────────────────────────────────────────────────┤
│ 8. Destruction (on shutdown)                                 │
│    - @PreDestroy methods                                     │
│    - DisposableBean.destroy()                                │
│    - Custom destroy-method                                   │
└─────────────────────────────────────────────────────────────┘
@Component
public class LifecycleDemoBean implements BeanNameAware, InitializingBean,
        DisposableBean, ApplicationContextAware {

    private String beanName;
    private ApplicationContext context;

    public LifecycleDemoBean() {
        System.out.println("1. Constructor");
    }

    @Autowired
    public void setDependency(SomeDependency dep) {
        System.out.println("2. Dependency injection");
    }

    @Override
    public void setBeanName(String name) {
        this.beanName = name;
        System.out.println("3. BeanNameAware: " + name);
    }

    @Override
    public void setApplicationContext(ApplicationContext ctx) {
        this.context = ctx;
        System.out.println("3. ApplicationContextAware");
    }

    @PostConstruct
    public void postConstruct() {
        System.out.println("4. @PostConstruct");
    }

    @Override
    public void afterPropertiesSet() {
        System.out.println("5. InitializingBean.afterPropertiesSet");
    }

    @PreDestroy
    public void preDestroy() {
        System.out.println("8. @PreDestroy");
    }

    @Override
    public void destroy() {
        System.out.println("8. DisposableBean.destroy");
    }
}

2. Custom Starters

Creating custom starters is a senior-level skill for building reusable infrastructure.

Starter Structure

my-company-spring-boot-starter/
├── my-company-spring-boot-autoconfigure/     # Auto-configuration module
│   ├── src/main/java/
│   │   └── com/company/autoconfigure/
│   │       ├── MyServiceAutoConfiguration.java
│   │       ├── MyServiceProperties.java
│   │       └── MyService.java
│   ├── src/main/resources/
│   │   └── META-INF/
│   │       └── spring/
│   │           └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│   └── pom.xml
│
└── my-company-spring-boot-starter/           # Starter module (dependencies only)
    └── pom.xml

Building an Auto-Configuration

// 1. Configuration properties
@ConfigurationProperties(prefix = "company.service")
public class MyServiceProperties {

    private boolean enabled = true;
    private String endpoint = "https://api.company.com";
    private Duration timeout = Duration.ofSeconds(30);
    private RetryConfig retry = new RetryConfig();

    // Nested configuration
    public static class RetryConfig {
        private int maxAttempts = 3;
        private Duration backoff = Duration.ofMillis(100);

        // Getters and setters
    }

    // Getters and setters
}

// 2. The service being auto-configured
public class MyService {

    private final MyServiceProperties properties;
    private final RestClient restClient;

    public MyService(MyServiceProperties properties, RestClient restClient) {
        this.properties = properties;
        this.restClient = restClient;
    }

    public Response callApi(Request request) {
        return restClient.post()
            .uri(properties.getEndpoint())
            .body(request)
            .retrieve()
            .body(Response.class);
    }
}

// 3. Auto-configuration class
@AutoConfiguration
@ConditionalOnClass(MyService.class)
@ConditionalOnProperty(
    prefix = "company.service",
    name = "enabled",
    havingValue = "true",
    matchIfMissing = true
)
@EnableConfigurationProperties(MyServiceProperties.class)
public class MyServiceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public MyService myService(MyServiceProperties properties,
                               ObjectProvider<RestClient.Builder> restClientBuilder) {
        RestClient restClient = restClientBuilder
            .getIfAvailable(RestClient::builder)
            .requestFactory(new JdkClientHttpRequestFactory())
            .build();

        return new MyService(properties, restClient);
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "company.service.retry", name = "enabled",
                          havingValue = "true", matchIfMissing = true)
    public RetryTemplate myServiceRetryTemplate(MyServiceProperties properties) {
        return RetryTemplate.builder()
            .maxAttempts(properties.getRetry().getMaxAttempts())
            .exponentialBackoff(
                properties.getRetry().getBackoff().toMillis(),
                2.0,
                30000)
            .build();
    }
}

Registration File

# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.company.autoconfigure.MyServiceAutoConfiguration

Starter POM

<!-- my-company-spring-boot-starter/pom.xml -->
<project>
    <artifactId>my-company-spring-boot-starter</artifactId>

    <dependencies>
        <!-- Pull in the autoconfigure module -->
        <dependency>
            <groupId>com.company</groupId>
            <artifactId>my-company-spring-boot-autoconfigure</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!-- Required dependencies for users -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

Configuration Metadata

// Enable IDE auto-completion for properties
// Add spring-boot-configuration-processor dependency

@ConfigurationProperties(prefix = "company.service")
public class MyServiceProperties {

    /**
     * Enable or disable the company service integration.
     */
    private boolean enabled = true;

    /**
     * Base URL for the company API.
     */
    private String endpoint = "https://api.company.com";
}
// META-INF/additional-spring-configuration-metadata.json
{
  "properties": [
    {
      "name": "company.service.endpoint",
      "type": "java.lang.String",
      "description": "Base URL for the company API.",
      "defaultValue": "https://api.company.com"
    }
  ],
  "hints": [
    {
      "name": "company.service.endpoint",
      "values": [
        {
          "value": "https://api.company.com",
          "description": "Production endpoint"
        },
        {
          "value": "https://sandbox.company.com",
          "description": "Sandbox endpoint"
        }
      ]
    }
  ]
}

3. Advanced Configuration

Property Sources Hierarchy

What is the order of property source precedence?

Property Source Precedence (highest to lowest):
┌─────────────────────────────────────────────────────────────┐
│ 1. Command line arguments (--server.port=8081)              │
├─────────────────────────────────────────────────────────────┤
│ 2. SPRING_APPLICATION_JSON (inline JSON)                    │
├─────────────────────────────────────────────────────────────┤
│ 3. ServletConfig/ServletContext parameters                  │
├─────────────────────────────────────────────────────────────┤
│ 4. JNDI attributes                                          │
├─────────────────────────────────────────────────────────────┤
│ 5. Java System properties (-Dserver.port=8081)              │
├─────────────────────────────────────────────────────────────┤
│ 6. OS environment variables (SERVER_PORT=8081)              │
├─────────────────────────────────────────────────────────────┤
│ 7. Profile-specific properties (application-{profile}.yml)  │
├─────────────────────────────────────────────────────────────┤
│ 8. Application properties (application.yml)                 │
├─────────────────────────────────────────────────────────────┤
│ 9. @PropertySource annotations                              │
├─────────────────────────────────────────────────────────────┤
│ 10. Default properties (SpringApplication.setDefaultProps)  │
└─────────────────────────────────────────────────────────────┘

Profile-Based Configuration

# application.yml - Common settings
spring:
  application:
    name: my-service

---
# application-local.yml
spring:
  config:
    activate:
      on-profile: local
  datasource:
    url: jdbc:h2:mem:testdb
logging:
  level:
    com.company: DEBUG

---
# application-production.yml
spring:
  config:
    activate:
      on-profile: production
  datasource:
    url: jdbc:postgresql://prod-db:5432/myapp
    hikari:
      maximum-pool-size: 20
logging:
  level:
    root: WARN
    com.company: INFO
// Profile-specific beans
@Configuration
public class DataSourceConfig {

    @Bean
    @Profile("local")
    public DataSource h2DataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .build();
    }

    @Bean
    @Profile("production")
    public DataSource productionDataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder()
            .type(HikariDataSource.class)
            .build();
    }
}

// Programmatic profile activation
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Application.class);

        if (System.getenv("KUBERNETES_SERVICE_HOST") != null) {
            app.setAdditionalProfiles("kubernetes");
        }

        app.run(args);
    }
}

Type-Safe Configuration

@ConfigurationProperties(prefix = "app.features")
@Validated
public class FeatureProperties {

    @NotNull
    private Map<String, FeatureFlag> flags = new HashMap<>();

    @Valid
    private RateLimiting rateLimiting = new RateLimiting();

    public static class FeatureFlag {
        private boolean enabled = false;
        private Set<String> allowedUsers = new HashSet<>();
        private LocalDateTime enabledUntil;

        // Getters and setters
    }

    public static class RateLimiting {
        @Min(1)
        @Max(10000)
        private int requestsPerMinute = 100;

        @DurationUnit(ChronoUnit.SECONDS)
        private Duration window = Duration.ofMinutes(1);

        // Getters and setters
    }
}

// Usage
@Service
@RequiredArgsConstructor
public class FeatureService {
    private final FeatureProperties features;

    public boolean isFeatureEnabled(String featureName, String userId) {
        FeatureFlag flag = features.getFlags().get(featureName);
        if (flag == null || !flag.isEnabled()) {
            return false;
        }
        if (!flag.getAllowedUsers().isEmpty() &&
            !flag.getAllowedUsers().contains(userId)) {
            return false;
        }
        if (flag.getEnabledUntil() != null &&
            LocalDateTime.now().isAfter(flag.getEnabledUntil())) {
            return false;
        }
        return true;
    }
}

4. Spring Cloud Essentials

Spring Cloud provides tools for distributed systems patterns.

Config Server

# Config Server application.yml
spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          uri: https://github.com/company/config-repo
          search-paths: '{application}'
          default-label: main
        encrypt:
          enabled: true

encrypt:
  key: ${ENCRYPT_KEY}  # Or use keystore for asymmetric

server:
  port: 8888
# Client application.yml
spring:
  application:
    name: order-service
  config:
    import: "configserver:http://config-server:8888"
  cloud:
    config:
      fail-fast: true
      retry:
        max-attempts: 6
        initial-interval: 1000
        multiplier: 1.5
// Refresh configuration at runtime
@RefreshScope
@Service
public class PricingService {

    @Value("${pricing.discount-percentage:0}")
    private double discountPercentage;

    public BigDecimal calculatePrice(BigDecimal basePrice) {
        BigDecimal discount = basePrice.multiply(
            BigDecimal.valueOf(discountPercentage / 100));
        return basePrice.subtract(discount);
    }
}

// Trigger refresh via actuator
// POST /actuator/refresh

// Or use Spring Cloud Bus for cluster-wide refresh
// POST /actuator/busrefresh

Service Discovery with Eureka

# Eureka Server
spring:
  application:
    name: eureka-server

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
  server:
    enable-self-preservation: false  # Disable in dev

---
# Service Client
spring:
  application:
    name: order-service

eureka:
  client:
    service-url:
      defaultZone: http://eureka:8761/eureka/
  instance:
    prefer-ip-address: true
    lease-renewal-interval-in-seconds: 10
// Load-balanced RestClient
@Configuration
public class RestClientConfig {

    @Bean
    @LoadBalanced
    public RestClient.Builder loadBalancedRestClientBuilder() {
        return RestClient.builder();
    }
}

@Service
public class UserClient {

    private final RestClient restClient;

    public UserClient(RestClient.Builder builder) {
        this.restClient = builder
            .baseUrl("http://user-service")  // Service name, not URL
            .build();
    }

    public User getUser(Long id) {
        return restClient.get()
            .uri("/api/users/{id}", id)
            .retrieve()
            .body(User.class);
    }
}

Spring Cloud Gateway

spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - StripPrefix=1
            - name: CircuitBreaker
              args:
                name: orderServiceCB
                fallbackUri: forward:/fallback/orders
            - name: Retry
              args:
                retries: 3
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
                methods: GET
                backoff:
                  firstBackoff: 50ms
                  maxBackoff: 500ms
                  factor: 2

        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/users/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                key-resolver: "#{@userKeyResolver}"
// Custom filters
@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getHeaders()
            .getFirst(HttpHeaders.AUTHORIZATION);

        if (token == null || !token.startsWith("Bearer ")) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        // Validate token and add user info to headers
        String userId = validateAndExtractUserId(token);
        ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
            .header("X-User-Id", userId)
            .build();

        return chain.filter(exchange.mutate().request(modifiedRequest).build());
    }

    @Override
    public int getOrder() {
        return -100;  // Run early
    }
}

Distributed Tracing

# application.yml
spring:
  application:
    name: order-service

management:
  tracing:
    sampling:
      probability: 1.0  # Sample all requests (reduce in production)
  zipkin:
    tracing:
      endpoint: http://zipkin:9411/api/v2/spans
// Traces propagate automatically through:
// - RestClient/WebClient (with instrumentation)
// - Kafka (with tracing headers)
// - @Async methods

// Manual span creation
@Service
@RequiredArgsConstructor
public class OrderService {

    private final Tracer tracer;

    public Order processOrder(Order order) {
        Span span = tracer.nextSpan().name("process-order").start();
        try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
            span.tag("order.id", order.getId().toString());
            span.tag("order.amount", order.getAmount().toString());

            // Processing logic
            validateOrder(order);
            reserveInventory(order);
            processPayment(order);

            span.event("order-completed");
            return order;
        } catch (Exception e) {
            span.error(e);
            throw e;
        } finally {
            span.end();
        }
    }
}

5. Reactive Spring

WebFlux enables non-blocking, reactive applications.

WebFlux vs MVC

Spring MVC (Blocking):
┌────────────┐    ┌────────────┐    ┌────────────┐
│  Request   │───▶│   Thread   │───▶│  Database  │
│            │    │  (blocked) │    │   Query    │
│            │◀───│   waits    │◀───│            │
│  Response  │    │            │    │            │
└────────────┘    └────────────┘    └────────────┘
Thread pool: 200 threads = 200 concurrent requests max

Spring WebFlux (Non-Blocking):
┌────────────┐    ┌────────────┐    ┌────────────┐
│ Request 1  │───▶│            │───▶│  Database  │
│ Request 2  │───▶│  Event     │───▶│   Query    │
│ Request 3  │───▶│  Loop      │───▶│  (async)   │
│    ...     │───▶│ (few thds) │◀───│            │
│ Request N  │◀───│            │◀───│            │
└────────────┘    └────────────┘    └────────────┘
Few threads can handle thousands of concurrent requests

Reactive Controllers

@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;

    // Return Mono for single value
    @GetMapping("/{id}")
    public Mono<Order> getOrder(@PathVariable String id) {
        return orderService.findById(id);
    }

    // Return Flux for multiple values (streaming)
    @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<Order> streamOrders() {
        return orderService.findAll()
            .delayElements(Duration.ofMillis(100));  // Simulate streaming
    }

    // Reactive request body
    @PostMapping
    public Mono<Order> createOrder(@RequestBody Mono<CreateOrderRequest> request) {
        return request
            .flatMap(orderService::create)
            .doOnSuccess(order -> log.info("Created order: {}", order.getId()));
    }

    // Error handling
    @GetMapping("/{id}/details")
    public Mono<OrderDetails> getOrderDetails(@PathVariable String id) {
        return orderService.findById(id)
            .switchIfEmpty(Mono.error(new OrderNotFoundException(id)))
            .flatMap(this::enrichWithDetails)
            .timeout(Duration.ofSeconds(5))
            .onErrorResume(TimeoutException.class,
                e -> Mono.error(new ServiceUnavailableException("Timeout")));
    }
}

Reactive Database Access with R2DBC

// Repository
public interface OrderRepository extends ReactiveCrudRepository<Order, String> {

    Flux<Order> findByCustomerId(String customerId);

    @Query("SELECT * FROM orders WHERE status = :status ORDER BY created_at DESC LIMIT :limit")
    Flux<Order> findRecentByStatus(String status, int limit);
}

// Service with transactions
@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final TransactionalOperator transactionalOperator;

    public Mono<Order> createOrder(CreateOrderRequest request) {
        return Mono.just(request)
            .map(this::mapToOrder)
            .flatMap(orderRepository::save)
            .flatMap(this::reserveInventory)
            .as(transactionalOperator::transactional);  // Reactive transaction
    }
}

// Configuration
@Configuration
@EnableR2dbcRepositories
public class R2dbcConfig extends AbstractR2dbcConfiguration {

    @Override
    @Bean
    public ConnectionFactory connectionFactory() {
        return ConnectionFactories.get(ConnectionFactoryOptions.builder()
            .option(DRIVER, "postgresql")
            .option(HOST, "localhost")
            .option(PORT, 5432)
            .option(DATABASE, "orders")
            .option(USER, "user")
            .option(PASSWORD, "password")
            .build());
    }
}

WebClient for Reactive HTTP

@Service
public class ExternalApiClient {

    private final WebClient webClient;

    public ExternalApiClient(WebClient.Builder builder) {
        this.webClient = builder
            .baseUrl("https://api.external.com")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .filter(ExchangeFilterFunction.ofRequestProcessor(request -> {
                log.debug("Request: {} {}", request.method(), request.url());
                return Mono.just(request);
            }))
            .build();
    }

    public Mono<ExternalData> fetchData(String id) {
        return webClient.get()
            .uri("/data/{id}", id)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError,
                response -> Mono.error(new ClientException("Client error")))
            .onStatus(HttpStatusCode::is5xxServerError,
                response -> Mono.error(new ServerException("Server error")))
            .bodyToMono(ExternalData.class)
            .retryWhen(Retry.backoff(3, Duration.ofMillis(100))
                .filter(e -> e instanceof ServerException));
    }

    // Parallel calls
    public Mono<AggregatedData> fetchAggregated(String userId) {
        Mono<UserProfile> profileMono = fetchUserProfile(userId);
        Mono<List<Order>> ordersMono = fetchUserOrders(userId).collectList();
        Mono<Preferences> prefsMono = fetchPreferences(userId);

        return Mono.zip(profileMono, ordersMono, prefsMono)
            .map(tuple -> new AggregatedData(
                tuple.getT1(),
                tuple.getT2(),
                tuple.getT3()
            ));
    }
}

6. Production Patterns

Actuator Customization

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
      base-path: /management
  endpoint:
    health:
      show-details: when_authorized
      probes:
        enabled: true  # Kubernetes probes
  health:
    diskspace:
      threshold: 10GB
  info:
    env:
      enabled: true
    git:
      mode: full

# Custom info
info:
  app:
    name: ${spring.application.name}
    version: @project.version@
    encoding: @project.build.sourceEncoding@
// Custom health indicator
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final PaymentGatewayClient client;

    @Override
    public Health health() {
        try {
            HealthCheckResponse response = client.healthCheck();
            if (response.isHealthy()) {
                return Health.up()
                    .withDetail("gateway", "Payment gateway is responsive")
                    .withDetail("latency", response.getLatencyMs() + "ms")
                    .build();
            } else {
                return Health.down()
                    .withDetail("gateway", "Payment gateway reports unhealthy")
                    .withDetail("reason", response.getReason())
                    .build();
            }
        } catch (Exception e) {
            return Health.down()
                .withDetail("gateway", "Cannot reach payment gateway")
                .withException(e)
                .build();
        }
    }
}

// Custom metrics
@Component
@RequiredArgsConstructor
public class OrderMetrics {

    private final MeterRegistry registry;
    private final AtomicLong activeOrders = new AtomicLong(0);

    @PostConstruct
    public void init() {
        Gauge.builder("orders.active", activeOrders, AtomicLong::get)
            .description("Number of orders being processed")
            .register(registry);
    }

    public void recordOrderCreated(Order order) {
        registry.counter("orders.created",
            "type", order.getType().name(),
            "region", order.getRegion()
        ).increment();

        activeOrders.incrementAndGet();
    }

    public void recordOrderCompleted(Order order, long processingTimeMs) {
        registry.timer("orders.processing.time",
            "type", order.getType().name(),
            "status", order.getStatus().name()
        ).record(Duration.ofMillis(processingTimeMs));

        activeOrders.decrementAndGet();
    }
}

Graceful Shutdown

server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s
@Component
@RequiredArgsConstructor
public class GracefulShutdownHandler implements SmartLifecycle {

    private final OrderProcessor orderProcessor;
    private boolean running = false;

    @Override
    public void start() {
        running = true;
    }

    @Override
    public void stop(Runnable callback) {
        log.info("Initiating graceful shutdown...");

        // Stop accepting new work
        orderProcessor.stopAcceptingOrders();

        // Wait for in-flight orders to complete
        try {
            orderProcessor.awaitCompletion(Duration.ofSeconds(25));
            log.info("All orders processed, shutting down");
        } catch (InterruptedException e) {
            log.warn("Shutdown interrupted, some orders may be incomplete");
            Thread.currentThread().interrupt();
        }

        running = false;
        callback.run();
    }

    @Override
    public boolean isRunning() {
        return running;
    }

    @Override
    public int getPhase() {
        return Integer.MAX_VALUE;  // Shut down last
    }
}

7. Performance & Scalability

Thread Pool Configuration

server:
  tomcat:
    threads:
      max: 200
      min-spare: 20
    accept-count: 100
    connection-timeout: 10s

spring:
  task:
    execution:
      pool:
        core-size: 8
        max-size: 50
        queue-capacity: 100
        keep-alive: 60s
      thread-name-prefix: async-
    scheduling:
      pool:
        size: 5
      thread-name-prefix: scheduled-
// Custom async executor
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("custom-async-");
        executor.setRejectedExecutionHandler(new CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(30);
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (throwable, method, params) -> {
            log.error("Async method {} threw exception: {}",
                method.getName(), throwable.getMessage(), throwable);
        };
    }
}

// Usage
@Service
public class NotificationService {

    @Async
    public CompletableFuture<Void> sendNotificationAsync(Notification notification) {
        // Non-blocking notification sending
        return CompletableFuture.runAsync(() -> {
            emailService.send(notification);
            pushService.send(notification);
        });
    }
}

Connection Pool Tuning

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      leak-detection-threshold: 60000
      pool-name: OrderServicePool
// Monitor connection pool
@Component
@RequiredArgsConstructor
public class ConnectionPoolMonitor {

    private final HikariDataSource dataSource;
    private final MeterRegistry registry;

    @Scheduled(fixedRate = 30000)
    public void reportMetrics() {
        HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();

        registry.gauge("hikari.connections.active",
            poolMXBean, HikariPoolMXBean::getActiveConnections);
        registry.gauge("hikari.connections.idle",
            poolMXBean, HikariPoolMXBean::getIdleConnections);
        registry.gauge("hikari.connections.pending",
            poolMXBean, HikariPoolMXBean::getThreadsAwaitingConnection);
    }
}

JVM Tuning Guidelines

# Production JVM settings
java -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -XX:+UseStringDeduplication \
     -Xms2g -Xmx2g \
     -XX:MetaspaceSize=256m \
     -XX:MaxMetaspaceSize=512m \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/log/app/heapdump.hprof \
     -Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=100m \
     -jar app.jar

# For low-latency (Java 21+)
java -XX:+UseZGC \
     -XX:+ZGenerational \
     -Xms4g -Xmx4g \
     -jar app.jar

Quick Reference: Senior Interview Topics

Topic Key Points
Auto-configuration spring.factories/imports, @Conditional, ordering
Custom starters Two-module structure, ConfigurationProperties, metadata
Bean lifecycle Instantiation → Population → Aware → Init → Destroy
Config precedence CLI args > env vars > profile properties > application.yml
Spring Cloud Config Config server, @RefreshScope, encryption
Service Discovery Eureka, @LoadBalanced, health checks
Gateway Routes, filters, rate limiting, circuit breakers
WebFlux vs MVC Non-blocking vs blocking, Mono/Flux, backpressure
Actuator Health indicators, custom metrics, securing endpoints
Performance Thread pools, connection pools, async processing

Related Articles

Ready for More Interview Questions?

This is just one topic from our complete interview prep guide. Get access to 800+ questions across 13 technologies.

Get Full Access Try Free Preview
Back to blog

Leave a comment

Please note, comments need to be approved before they are published.