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/busrefreshService 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.jarQuick 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
