Spring Boot Advanced Architecture Interview Guide: Senior-Level Deep Dive
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
- Spring Boot Interview Guide - Core Spring Boot fundamentals
- Microservices Architecture Interview Guide - Distributed systems patterns
- Java Core Interview Guide - Java language fundamentals
- Complete Java Backend Developer Interview Guide - Full Java backend preparation

Written by
EasyInterview Team
Based on 15+ years in tech and hundreds of technical interviews conducted at companies like BNY Mellon, UBS, and leading fintech firms.
Ready for More Interview Questions?
This is just one topic from our complete interview prep guide. Get access to 800+ questions across 13 technologies.