Spring Boot Fundamentals & Internals Interview Questions
How Spring Boot auto-configuration actually works internally, dependency injection trade-offs, and the questions that separate a candidate who's used annotations from one who understands the container.
Q1. How does Spring Boot auto-configuration actually work internally?
Auto-configuration automatically wires beans based on what's on the classpath, but the mechanism behind that is worth being able to explain past the one-sentence version.
1. Entry point — @SpringBootApplication
@SpringBootApplication
// equivalent to:
@SpringBootConfiguration // = @Configuration
@EnableAutoConfiguration // triggers auto-config
@ComponentScan // scans for components
public class Application {}
2. @EnableAutoConfiguration mechanism
SpringFactoriesLoaderreadsMETA-INF/spring.factories(or, on newer Boot versions,META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports) from every JAR on the classpath- Loads every class listed under
org.springframework.boot.autoconfigure.EnableAutoConfiguration - Each auto-configuration class is then evaluated
3. Conditional annotations decide whether each one actually applies
@ConditionalOnClass— only if specific classes exist on the classpath@ConditionalOnMissingBean— only if a bean of that type doesn't already exist@ConditionalOnProperty— based on a configuration property@ConditionalOnWebApplication— only for web applications
4. A real auto-configuration class looks like this
@Configuration
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}
The key point interviewers are listening for: your own @Bean definitions always win, because the auto-configured ones are guarded by @ConditionalOnMissingBean. Spring Boot never silently overrides something you explicitly configured.
To see this happening live, set debug=true in application.properties — it prints every auto-configuration class Spring evaluated and whether it matched or was excluded, which is the fastest way to actually debug "why isn't my bean being created."
Q2. Compare constructor, setter, and field injection — which do you actually use, and why?
All three wire the same dependency, but they're not equivalent in practice.
// Constructor injection — preferred
@Service
public class OrderService {
private final PaymentClient paymentClient;
public OrderService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
}
// Field injection — avoid in production code
@Service
public class OrderService {
@Autowired
private PaymentClient paymentClient;
}
Constructor injection wins for three concrete reasons, not just convention:
- Immutability — the field can be
final, so it's impossible to reassign it after construction, and the class can't exist in a half-initialized state. - Fails fast at startup, not at runtime — if a required dependency is missing, the application context fails to start with a clear error, instead of throwing a
NullPointerExceptionthe first time the field is actually used. - Testability without a Spring context — you can construct the object directly with
new OrderService(mockPaymentClient)in a unit test. Field injection needs reflection or a full Spring context to populate the dependency at all.
Field injection's only real argument in its favor is less boilerplate, which modern IDEs and Lombok's @RequiredArgsConstructor eliminate anyway — so there's rarely a good reason to reach for it in anything beyond a quick prototype.
Q3. What are Spring Profiles and how do you use them effectively in microservices?
Spring Profiles allow you to segregate application configuration and make it available only in certain environments.
1. Defining Profiles
// application.properties (common config)
app.name=MyService
// application-dev.properties
spring.datasource.url=jdbc:h2:mem:testdb
logging.level.root=DEBUG
// application-prod.properties
spring.datasource.url=jdbc:mysql://prod-db:3306/mydb
logging.level.root=WARN
2. Activating Profiles
// Method 1: application.properties
spring.profiles.active=dev
// Method 2: Command line
java -jar app.jar --spring.profiles.active=prod
// Method 3: Environment variable
export SPRING_PROFILES_ACTIVE=prod
// Method 4: Programmatically
SpringApplication app = new SpringApplication(Application.class);
app.setAdditionalProfiles("dev");
app.run(args);
3. Profile-Specific Beans
@Configuration
@Profile("dev")
public class DevConfig {
@Bean
public DataSource dataSource() {
return new H2DataSource(); // In-memory for dev
}
}
@Configuration
@Profile("prod")
public class ProdConfig {
@Bean
public DataSource dataSource() {
return new MySQLDataSource(); // Production DB
}
}
4. Multiple Profiles
// Activate multiple profiles
spring.profiles.active=dev,debug,local
// Profile expressions
@Profile("!prod") // NOT prod
@Profile("dev | test") // dev OR test
@Profile("cloud & mysql") // cloud AND mysql
5. YAML Configuration (Better for profiles)
spring:
profiles:
active: dev
---
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:testdb
---
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:mysql://prod-db:3306/mydb
6. Microservices Best Practices
- local: Developer machine (H2, mocks)
- dev: Development environment
- qa: QA/Testing environment
- staging: Pre-production
- prod: Production
7. Profile Groups (Spring Boot 2.4+)
spring:
profiles:
group:
production:
- "prod"
- "metrics"
- "monitoring"
local:
- "dev"
- "debug"
- "h2"
# Activate: spring.profiles.active=production
# This activates: prod, metrics, monitoring
8. Checking Active Profile in Code
@Component
public class ProfileChecker {
@Autowired
private Environment environment;
public void checkProfile() {
String[] activeProfiles = environment.getActiveProfiles();
boolean isProd = environment.acceptsProfiles(Profiles.of("prod"));
if (isProd) {
// Production-specific logic
}
}
}
Profiles are useful for selecting environment-specific wiring, but they should not become a substitute for externalized secrets or a long list of hidden production switches. The senior answer is to keep profile differences small, explicit, and safe to verify in deployment.
Q4. How does Spring resolve which bean to inject when multiple implementations exist?
The constructor-vs-setter-vs-field decision above settles how a dependency gets wired in. This question is different: once there are two or more beans of the same type on the classpath, Spring needs a way to know which one you mean, or it fails to start with a NoUniqueBeanDefinitionException.
1. @Qualifier for Multiple Beans
// Define multiple implementations
@Component("stripeProcessor")
public class StripeProcessor implements PaymentProcessor {}
@Component("paypalProcessor")
public class PaypalProcessor implements PaymentProcessor {}
// Use @Qualifier
@Service
public class PaymentService {
private final PaymentProcessor processor;
public PaymentService(@Qualifier("stripeProcessor")
PaymentProcessor processor) {
this.processor = processor;
}
}
2. @Primary Annotation
@Component
@Primary // Default choice when no @Qualifier specified
public class DefaultPaymentProcessor implements PaymentProcessor {}
3. Custom Qualifiers
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface PaymentType {
String value();
}
@Component
@PaymentType("stripe")
public class StripeProcessor implements PaymentProcessor {}
// Usage:
public PaymentService(@PaymentType("stripe")
PaymentProcessor processor) {}
@Qualifier is explicit at every injection site, @Primary sets a quiet default that's easy to forget about six months later, and a custom qualifier annotation is worth the extra ceremony once you have more than two or three implementations of the same interface and @Qualifier("stripeProcessor") string literals start drifting out of sync with the actual bean names.
Q5. Explain @Configuration vs @Component. When to use which?
@Component
@Component
public class UserService {
// Spring creates ONE instance
// Managed as singleton bean
}
Use for: Regular Spring-managed beans, services, repositories
@Configuration
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
@Bean
public UserService userService() {
return new UserService(dataSource()); // Proxied!
}
}
Key Difference: CGLIB Proxying
@Configuration // Creates CGLIB proxy
public class Config {
@Bean
public BeanA beanA() {
return new BeanA();
}
@Bean
public BeanB beanB() {
return new BeanB(beanA()); // Returns SAME instance from container
}
}
@Component // NO proxy
public class NotAConfig {
@Bean
public BeanA beanA() {
return new BeanA();
}
@Bean
public BeanB beanB() {
return new BeanB(beanA()); // Creates NEW instance
}
}
When to Use What:
| Scenario | Use |
|---|---|
| Define multiple @Bean methods | @Configuration |
| Beans depend on each other | @Configuration |
| Regular business logic | @Component |
| Third-party library beans | @Configuration + @Bean |
The distinction that matters in production is proxy behavior: use @Configuration when @Bean methods depend on one another and must return the container-managed singleton, and use @Component for ordinary component scanning.
Q6. How do you optimize Spring Boot application startup time?
1. Lazy Initialization
spring.main.lazy-initialization=true
Impact: 40-50% faster startup, but first request slower
2. Component Scan Optimization
@SpringBootApplication(scanBasePackages = "com.company.specific")
// Instead of scanning entire classpath
3. Exclude Unused Auto-Configurations
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class
})
4. Profile-Specific Loading
spring.profiles.active=prod
# Don't load dev/test configs in production
5. JVM Optimizations
java -jar app.jar \
-XX:TieredStopAtLevel=1 \ # Fast startup (dev)
-XX:+UseG1GC \ # Better GC
-Xms512m -Xmx512m # Fixed heap
6. Spring Boot 3+ Native Image
./mvnw -Pnative native:compile
# Result: <100ms startup
7. Database Connection Pool Tuning
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=20
Typical app startup can drop from 10–15 seconds to 3–5 seconds, lazy initialization can reduce that further to roughly 2–3 seconds, and a native image can start in under 100 milliseconds. The trade-off is that lazy initialization shifts some work to the first request, while native images add build complexity and constraints that only pay off when startup and memory are operational priorities.
The production detail worth mentioning is that startup optimization should be measured against readiness time and first-request latency, not treated as a race to the smallest number on a local machine.
Q7. Explain @SpringBootApplication annotation in detail.
@SpringBootApplication
// This is a convenience annotation equivalent to:
@SpringBootConfiguration // = @Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application {}
Breaking it down:
- @SpringBootConfiguration: Marks class as configuration source
- @EnableAutoConfiguration: Triggers auto-configuration
- @ComponentScan: Scans current package + sub-packages
Customization:
@SpringBootApplication(
scanBasePackages = {"com.app.core", "com.app.api"},
exclude = {DataSourceAutoConfiguration.class}
)
The interview point is that the annotation is a convenience composition, while its component scan and auto-configuration boundaries still determine what the application can see and which defaults Spring Boot applies.
Q8. What are Spring Boot Starters? How do they work?
Starters are dependency descriptors that bring in all required dependencies.
Common Starters:
spring-boot-starter-web // Web + Tomcat + JSON
spring-boot-starter-data-jpa // JPA + Hibernate
spring-boot-starter-security // Spring Security
spring-boot-starter-actuator // Monitoring
spring-boot-starter-test // Testing
What happens when you add starter-web:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
// Brings in:
// - spring-web
// - spring-webmvc
// - tomcat-embed-core
// - jackson-databind
// - validation-api
// + 20 more dependencies!
Name convention: xxx-spring-boot-starter
The useful distinction is that a starter manages a coherent dependency set; it does not itself create application behavior until the matching auto-configuration sees those dependencies on the classpath.