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

·15 min read
spring-bootspring-cloudjavaarchitectureseniormicroservicesinterview-preparation

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

TopicKey Points
Auto-configurationspring.factories/imports, @Conditional, ordering
Custom startersTwo-module structure, ConfigurationProperties, metadata
Bean lifecycleInstantiation → Population → Aware → Init → Destroy
Config precedenceCLI args > env vars > profile properties > application.yml
Spring Cloud ConfigConfig server, @RefreshScope, encryption
Service DiscoveryEureka, @LoadBalanced, health checks
GatewayRoutes, filters, rate limiting, circuit breakers
WebFlux vs MVCNon-blocking vs blocking, Mono/Flux, backpressure
ActuatorHealth indicators, custom metrics, securing endpoints
PerformanceThread pools, connection pools, async processing

Related Articles

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides