Go beyond using Spring — understand how it works under the hood
BeanFactory is the low-level IoC container; ApplicationContext extends it with enterprise features like event publishing, i18n, and annotation processing. Interviewers test whether you know ApplicationContext eagerly instantiates singletons while BeanFactory is lazy.
Understanding the full bean lifecycle — instantiation, populate properties, BeanNameAware, BeanFactoryAware, pre-init (BeanPostProcessor), InitializingBean/init-method, post-init, and destruction — is critical for diagnosing startup issues.
Bean scope controls how many instances Spring creates. Singleton (default) means one per ApplicationContext; prototype means a new instance on every lookup. Web scopes (request, session) are scoped to HTTP lifecycle.
Spring resolves circular dependencies for singleton setter/field injection via a three-level cache (singletonObjects, earlySingletonObjects, singletonFactories). Constructor injection circulars cannot be resolved and throw BeanCurrentlyInCreationException.
spring.main.allow-circular-references=true to re-enable@Lazy defers bean creation until first access, reducing startup time. @DependsOn forces bean initialization order without creating a direct dependency. Both are tools for controlling the lifecycle when auto-wiring order matters.
BeanPostProcessor intercepts every bean after instantiation (before and after init). BeanFactoryPostProcessor modifies bean definitions before any beans are created. These are how Spring implements AOP, @Value resolution, and @Autowired.
Spring AOP creates proxies to apply cross-cutting concerns. JDK proxies work on interfaces; CGLIB generates subclasses. Spring Boot defaults to CGLIB since 2.0. Knowing this explains why final classes/methods break AOP and self-invocation bypasses advice.
proxyTargetClass=true by default (CGLIB)Pointcut expressions define where advice applies. Mastering execution(), within(), @annotation(), and boolean combinators is essential for writing targeted cross-cutting logic in production.
execution(* com.example.service.*.*(..))&&, ||, ! for complex pointcuts@Pointcut methods improve readability and reuseAdvice types determine when your cross-cutting code runs relative to the join point. @Around is the most powerful — it can control whether the target method executes at all, modify arguments, or change the return value.
joinPoint.proceed() to execute itSpring AOP uses runtime proxy-based weaving by default. AspectJ supports compile-time (CTW) and load-time weaving (LTW), which can advise private methods, field access, and constructors — things Spring AOP cannot do.
@EnableLoadTimeWeaving@Transactional works through AOP proxies. Understanding the proxy chain explains why self-invocation skips transactions, why private @Transactional methods do nothing, and how propagation nests work in practice.
@SpringBootApplication is a convenience meta-annotation combining @Configuration, @EnableAutoConfiguration, and @ComponentScan. Interviewers expect you to explain what each does and why the main class location matters for scanning.
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)This annotation triggers the loading of auto-configuration classes from META-INF/spring.factories (or the new AutoConfiguration.imports file). It is the engine behind Spring Boot's "opinionated defaults" philosophy.
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports--debug flag or Actuator's conditions endpointspring.factories is the SPI mechanism Spring Boot uses to discover auto-configuration classes, listeners, and initializers. Understanding this file explains how starters work and how to write your own.
META-INF/spring.factories in each jar on the classpath@Conditional and its variants (@ConditionalOnClass, @ConditionalOnProperty, @ConditionalOnMissingBean, etc.) control whether a configuration class or bean is registered. This is how Spring Boot provides smart defaults that back off when you define your own beans.
Condition interface with a matches() method/actuator/conditions to see which conditions matched and which did notType-safe configuration binding that maps application.properties/yml prefixes to POJOs. Much cleaner than scattered @Value annotations and supports validation, nested objects, and immutable binding.
@ConfigurationProperties(prefix = "app.mail") and register it with @EnableConfigurationProperties@Validated with JSR-303 annotations for startup-time validation@ConstructorBinding (default in records since Boot 3.x)@Profile lets you conditionally register beans for specific environments (dev, staging, prod). Essential for configuring different data sources, feature flags, or mock services per environment.
spring.profiles.active=dev in properties, env vars, or command lineapplication-dev.yml overrides application.yml@Profile("!prod") — active in all environments except prodThe /actuator/health endpoint aggregates health checks for DB, disk, Redis, and other dependencies. Kubernetes readiness/liveness probes depend on this. Know how to configure groups and when to expose details.
management.endpoint.health.group.readiness.include=db,redismanagement.endpoint.health.show-details=when-authorizedImplement HealthIndicator to add custom checks (e.g., external API availability, queue depth, license validity). This is commonly asked in SDE 2 interviews to test your understanding of production-readiness patterns.
HealthIndicator interface and override health() methodHealth.up().withDetail("key", value).build() or Health.down()ReactiveHealthIndicator insteadSpring Boot Actuator integrates with Micrometer to expose JVM, HTTP, and custom metrics in formats compatible with Prometheus, Datadog, and others. Understanding this is key for production monitoring discussions.
micrometer-registry-prometheus dependency and hit /actuator/prometheusMeterRegistry and create counters, gauges, timers, or distribution summariesThe /actuator/shutdown endpoint triggers a graceful application shutdown. It is disabled by default for safety. In interviews, this tests your awareness of operational security and safe exposure practices.
management.endpoint.shutdown.enabled=true — disabled by defaultmanagement.endpoints.web.exposure.include to whitelist only needed endpointsmanagement.server.port=8081SecurityFilterChain is the modern way (Spring Security 5.4+) to configure HTTP security, replacing WebSecurityConfigurerAdapter. It is a bean that maps a request matcher to a list of security filters.
@Bean returning SecurityFilterChain from HttpSecurityWebSecurityConfigurerAdapter patternA base class guaranteeing a filter executes exactly once per request, even with request dispatches (forwards, includes). Use it for custom JWT validation filters, logging, or request enrichment.
doFilterInternal(request, response, filterChain)shouldNotFilter(request) to skip certain paths (e.g., public endpoints)Catches AccessDeniedException and AuthenticationException thrown by downstream filters and converts them into HTTP responses (401/403). It is the bridge between Spring Security exceptions and HTTP error handling.
AuthenticationEntryPoint (typically 401)AccessDeniedHandler (typically 403)The AuthorizationFilter (replacing FilterSecurityInterceptor in Spring Security 6) makes the final authorization decision for each request based on the configured access rules (permitAll, hasRole, etc.).
FilterSecurityInterceptor in Spring Security 6 for a simpler modelAuthorizationManager instead of the older AccessDecisionManager/Voter patternhttp.authorizeHttpRequests()Stateless JWT authentication eliminates server-side session storage. The token carries claims (user identity, roles) and is validated on every request. This is the most common auth pattern in microservices and a frequent SDE 2 interview topic.
http.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))OncePerRequestFilter that extracts, validates, and sets the SecurityContextSpring Security's OAuth2 Resource Server support validates bearer tokens (JWT or opaque) against an authorization server. It handles token parsing, signature verification, and authority mapping out of the box.
spring-boot-starter-oauth2-resource-server and configure spring.security.oauth2.resourceserver.jwt.issuer-uriJwtAuthenticationConverter@PreAuthorize enables SpEL-based authorization on individual methods, complementing URL-based security. This allows fine-grained access control at the service layer, not just at the controller level.
@EnableMethodSecurity (replaces @EnableGlobalMethodSecurity in Security 6)@PreAuthorize("hasRole('ADMIN') and #userId == authentication.principal.id")Cross-Origin Resource Sharing controls which origins can call your API from browsers. Misconfigured CORS is a common source of bugs in frontend-backend setups and a practical SDE 2 interview question.
http.cors(c -> c.configurationSource(...)) in SecurityFilterChain@CrossOrigin(origins = "https://example.com")allowedOrigins("*") with allowCredentials(true) — browsers block itCSRF protection prevents attackers from forging requests using a victim's authenticated session. Spring Security enables it by default. Knowing when it is safe to disable (stateless APIs) is a key security design question.
CookieCsrfTokenRepository.withHttpOnlyFalse() so JavaScript can read the tokenhttp.csrf(csrf -> csrf.disable()) — only for truly stateless APIsPropagation defines how transactions relate when methods call each other. REQUIRED joins an existing transaction; REQUIRES_NEW suspends it and starts a new one; NESTED creates a savepoint. This is one of the most frequently asked Spring topics.
Isolation levels control the visibility of concurrent transactions' uncommitted changes. Spring maps to the underlying database isolation via @Transactional(isolation = ...). Knowing the trade-offs between consistency and concurrency is critical.
This exception occurs when you access a lazily-loaded JPA association outside an active Hibernate session/transaction. It is one of the most common JPA bugs and a frequent interview topic.
The N+1 problem occurs when fetching a list of entities triggers N additional queries for their lazy associations. @EntityGraph and JOIN FETCH are the primary solutions. This is arguably the most common JPA performance question.
SELECT o FROM Order o JOIN FETCH o.items — loads everything in one query@EntityGraph(attributePaths = {"items"}) on repository methodspring.jpa.show-sql=true or p6spyJPQL operates on entity objects and is database-agnostic; native queries use raw SQL for database-specific features or performance optimization. Understanding when to use each is a practical SDE 2 concern.
@Query(nativeQuery = true) for native SQL in Spring Data repositories:paramName syntaxSpring Data JPA generates queries from method names: findByStatusAndCreatedAfter becomes a WHERE clause automatically. Understanding the parsing rules and limitations lets you avoid writing boilerplate queries.
findBy + property path + operator keywordAnd, Or, Between, LessThan, Like, In, OrderBy, etc.findByAddress_City(String city)The @Query annotation lets you write explicit JPQL or native SQL on repository methods. Use it when derived query method names become too long or when you need JOIN FETCH, aggregates, or complex predicates.
@Query("SELECT u FROM User u WHERE u.email = :email")@Modifying and @Transactional for UPDATE/DELETE#{#entityName} resolves to the entity class namePageable parameter and a countQueryProjections let you fetch only the columns you need instead of full entities. This reduces memory usage, network transfer, and avoids lazy loading issues. A key optimization pattern for production APIs.
SELECT new com.example.UserDTO(u.name, u.email) FROM User u<T> List<T> findByStatus(String status, Class<T> type)@Value("#{target.firstName + ' ' + target.lastName}") — flexible but lose query optimizationSpring Data provides Pageable and Sort abstractions that translate to LIMIT/OFFSET and ORDER BY clauses. Proper pagination is essential for any production API and a common practical interview question.
Pageable (via PageRequest.of(page, size, sort)) to any repository methodPage<T> (includes total count) or Slice<T> (no count query — more efficient)Sort.by(Sort.Direction.DESC, "createdAt") or via query derivation: findByStatusOrderByCreatedAtDesc@PageableDefault(size=20, sort="id") in controller method parameters