← Back to Tracker

Pillar 2 — Spring Boot & Framework Depth

Go beyond using Spring — understand how it works under the hood

Spring Core Internals Full-Focus

Interview Tip: These come up as "what happens when you use @Transactional on a private method" questions — a classic SDE 2 trap.

IoC & DI

BeanFactory vs ApplicationContext

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.

  • BeanFactory provides basic DI and lazy bean initialization
  • ApplicationContext adds AOP integration, event propagation, MessageSource for i18n
  • ApplicationContext pre-instantiates singletons at startup — fail-fast for misconfiguration
  • In modern Spring Boot apps you almost never interact with BeanFactory directly

Bean Lifecycle (init/destroy)

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.

  • Constructor → dependency injection → Aware interfaces → @PostConstruct → InitializingBean.afterPropertiesSet → custom init-method
  • Destroy order: @PreDestroy → DisposableBean.destroy → custom destroy-method
  • BeanPostProcessor hooks run for every bean — this is how AOP proxies get created
  • Prototype beans do NOT get destroy callbacks from the container

@Scope: singleton vs prototype vs request

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.

  • Singleton: shared across the entire context — thread-safety is your responsibility
  • Prototype: new instance per injection point or getBean() call — Spring does NOT manage its full lifecycle
  • Request/Session: require a web-aware ApplicationContext; use scoped proxies to inject into singletons
  • Injecting a prototype into a singleton gives you effectively a singleton prototype — use ObjectProvider or @Lookup

Circular Dependency Handling

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.

  • Three-level cache exposes early (partially initialized) references to break the cycle
  • Constructor injection circular dependency → always fails; redesign required
  • Spring Boot 2.6+ disallows circular dependencies by default — use spring.main.allow-circular-references=true to re-enable
  • Best practice: refactor to remove the cycle (extract a third bean or use events)

@Lazy & @DependsOn

@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.

  • @Lazy on a class or @Bean method — Spring creates a proxy; actual instance is created on first method call
  • @Lazy on an injection point can break circular dependencies by deferring one side
  • @DependsOn guarantees bean A is initialized before bean B — useful for side-effect ordering (e.g., DB migrations)
  • Overusing @Lazy hides startup errors — failures move to runtime

BeanPostProcessor / BeanFactoryPostProcessor

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.

  • BeanPostProcessor: postProcessBeforeInitialization / postProcessAfterInitialization — wraps beans with proxies
  • BeanFactoryPostProcessor: runs once, can modify BeanDefinition metadata (e.g., PropertySourcesPlaceholderConfigurer)
  • AutowiredAnnotationBeanPostProcessor handles @Autowired injection
  • If your BPP itself has @Autowired dependencies, those beans skip all other BPPs — order matters
  • CommonAnnotationBeanPostProcessor handles @PostConstruct and @Resource

AOP

JDK Dynamic Proxy vs CGLIB Proxy

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.

  • JDK proxy: requires an interface; uses java.lang.reflect.Proxy — faster creation, slightly slower invocation
  • CGLIB proxy: subclasses the target — works without interfaces but cannot proxy final methods/classes
  • Spring Boot sets proxyTargetClass=true by default (CGLIB)
  • Self-invocation problem: calling this.method() bypasses the proxy — advice does not apply

Pointcut Expressions

Pointcut expressions define where advice applies. Mastering execution(), within(), @annotation(), and boolean combinators is essential for writing targeted cross-cutting logic in production.

  • execution(): matches method signatures — execution(* com.example.service.*.*(..))
  • within(): matches all methods in a type or package
  • @annotation(): matches methods annotated with a specific annotation
  • Combine with &&, ||, ! for complex pointcuts
  • Named pointcuts via @Pointcut methods improve readability and reuse

@Around / @Before / @After

Advice 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.

  • @Before: runs before the method — cannot prevent execution (unless it throws)
  • @After: runs after the method regardless of outcome (like finally)
  • @AfterReturning: runs only on successful return — can access the return value
  • @AfterThrowing: runs only when an exception is thrown
  • @Around: wraps the method; must call joinPoint.proceed() to execute it

AspectJ Weaving Modes

Spring 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.

  • Spring AOP: runtime proxy weaving — limited to public method execution join points
  • Compile-time weaving (CTW): AspectJ compiler modifies bytecode at build time — full pointcut support
  • Load-time weaving (LTW): agent-based bytecode modification at classload — enabled via @EnableLoadTimeWeaving
  • CTW/LTW can intercept self-invocations, private methods, and field access

Transaction Proxy Chain

@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.

  • TransactionInterceptor is an @Around advice that opens/commits/rolls back transactions
  • The proxy intercepts the call before it reaches your bean — self-calls bypass it
  • Private methods annotated with @Transactional are silently ignored (CGLIB cannot override them)
  • Multiple @Transactional beans calling each other — each call goes through its own proxy, enabling REQUIRES_NEW semantics
  • Exception handling: by default only unchecked exceptions trigger rollback

Spring Boot Autoconfiguration Both

Mechanism

@SpringBootApplication Breakdown

@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.

  • @Configuration: marks the class as a source of bean definitions
  • @EnableAutoConfiguration: triggers Spring Boot's auto-configuration mechanism
  • @ComponentScan: scans the package and sub-packages of the annotated class
  • Placing the main class in the root package ensures all components are discovered
  • You can exclude specific auto-configs: @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

@EnableAutoConfiguration

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.

  • Uses AutoConfigurationImportSelector to load candidate configuration classes
  • Candidates are filtered by @Conditional annotations before being applied
  • Spring Boot 3.x migrated to META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  • You can see which auto-configs were applied via the --debug flag or Actuator's conditions endpoint

spring.factories / META-INF

spring.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.

  • Located at META-INF/spring.factories in each jar on the classpath
  • Key: fully-qualified interface name; Value: comma-separated list of implementations
  • SpringFactoriesLoader.loadFactoryNames() reads and caches all entries at startup
  • Spring Boot 3.x moved auto-configuration to a dedicated imports file for better performance
  • Starters bundle a spring.factories with their auto-config class — that's why adding a dependency "just works"

@Conditional Annotations

@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.

  • @ConditionalOnClass: activates only if a class is on the classpath (e.g., DataSource auto-config needs a JDBC driver)
  • @ConditionalOnMissingBean: backs off if you already defined a bean of that type
  • @ConditionalOnProperty: activates based on application.properties values
  • Custom conditions: implement the Condition interface with a matches() method
  • Use /actuator/conditions to see which conditions matched and which did not

@ConfigurationProperties

Type-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.

  • Annotate a class with @ConfigurationProperties(prefix = "app.mail") and register it with @EnableConfigurationProperties
  • Supports relaxed binding: app.mail-host, APP_MAIL_HOST, app.mailHost all bind to mailHost
  • Add @Validated with JSR-303 annotations for startup-time validation
  • Immutable binding via @ConstructorBinding (default in records since Boot 3.x)

@Profile Usage

@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.

  • Activate via spring.profiles.active=dev in properties, env vars, or command line
  • Profile-specific files: application-dev.yml overrides application.yml
  • Negate profiles: @Profile("!prod") — active in all environments except prod
  • Spring Boot 2.4+ introduced profile groups for activating multiple profiles together

Actuator

Health Endpoints

The /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.

  • Returns UP, DOWN, OUT_OF_SERVICE, or UNKNOWN based on aggregated indicator results
  • Auto-configured indicators for DataSource, Redis, Elasticsearch, RabbitMQ, etc.
  • Health groups: management.endpoint.health.group.readiness.include=db,redis
  • Show details: management.endpoint.health.show-details=when-authorized

Custom Health Indicators

Implement 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.

  • Implement HealthIndicator interface and override health() method
  • Return Health.up().withDetail("key", value).build() or Health.down()
  • The bean name minus "HealthIndicator" suffix becomes the component name in the response
  • For reactive apps, implement ReactiveHealthIndicator instead

Metrics Exposure

Spring 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 provides a vendor-neutral metrics facade (like SLF4J for metrics)
  • Built-in metrics: JVM memory, GC, HTTP request latency, connection pool stats
  • Expose to Prometheus: add micrometer-registry-prometheus dependency and hit /actuator/prometheus
  • Custom metrics: inject MeterRegistry and create counters, gauges, timers, or distribution summaries

Shutdown Endpoint Security

The /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.

  • Enable with management.endpoint.shutdown.enabled=true — disabled by default
  • Only accessible via POST — prevents accidental browser triggers
  • Never expose over the web without authentication — bind actuator to a separate port or restrict via Spring Security
  • Use management.endpoints.web.exposure.include to whitelist only needed endpoints
  • Consider running actuator on a different port: management.server.port=8081

Spring Security Full-Focus

Filter Chain

SecurityFilterChain

SecurityFilterChain 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.

  • Define as a @Bean returning SecurityFilterChain from HttpSecurity
  • Multiple SecurityFilterChain beans can coexist — matched by request path order
  • Replaced the deprecated WebSecurityConfigurerAdapter pattern
  • Each chain is an ordered list of Servlet Filters: CORS, CSRF, authentication, authorization, exception handling

OncePerRequestFilter

A 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.

  • Extends GenericFilterBean and adds a flag to prevent re-execution on forwards/includes
  • Override doFilterInternal(request, response, filterChain)
  • Override shouldNotFilter(request) to skip certain paths (e.g., public endpoints)
  • Common use: JWT token parsing filter inserted before UsernamePasswordAuthenticationFilter

ExceptionTranslationFilter

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.

  • Catches AuthenticationException → delegates to AuthenticationEntryPoint (typically 401)
  • Catches AccessDeniedException → delegates to AccessDeniedHandler (typically 403)
  • Saves the original request so the user can be redirected back after login
  • Sits after the authentication filters and before the authorization filter in the chain

AuthorizationFilter

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.).

  • Replaced FilterSecurityInterceptor in Spring Security 6 for a simpler model
  • Uses AuthorizationManager instead of the older AccessDecisionManager/Voter pattern
  • Evaluates rules defined in http.authorizeHttpRequests()
  • Runs late in the filter chain — authentication must already be established

Auth Patterns

JWT Stateless Auth

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.

  • Token structure: Header.Payload.Signature — Base64URL encoded, signed with HMAC or RSA
  • Disable sessions: http.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
  • Add a custom OncePerRequestFilter that extracts, validates, and sets the SecurityContext
  • Token expiry and refresh token rotation are critical for security
  • Trade-off: cannot revoke tokens without a blocklist or short expiry + refresh tokens

OAuth2 Resource Server

Spring 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.

  • Add spring-boot-starter-oauth2-resource-server and configure spring.security.oauth2.resourceserver.jwt.issuer-uri
  • Automatically fetches JWK Set from the authorization server for signature validation
  • Customize claim-to-authority mapping with a JwtAuthenticationConverter
  • Supports opaque token introspection as an alternative to JWT

Method-level Security (@PreAuthorize)

@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.

  • Enable with @EnableMethodSecurity (replaces @EnableGlobalMethodSecurity in Security 6)
  • SpEL expressions: @PreAuthorize("hasRole('ADMIN') and #userId == authentication.principal.id")
  • @PostAuthorize: evaluates after method execution — can check the return value
  • @Secured: simpler role-based alternative without SpEL support
  • Works via AOP proxies — self-invocation bypasses the check (same proxy caveat as @Transactional)

CORS Configuration

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.

  • Configure globally via http.cors(c -> c.configurationSource(...)) in SecurityFilterChain
  • Or per-controller with @CrossOrigin(origins = "https://example.com")
  • Preflight (OPTIONS) requests must be allowed — Spring handles this if CORS is properly configured
  • Never use allowedOrigins("*") with allowCredentials(true) — browsers block it

CSRF Tokens — When to Disable

CSRF 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.

  • CSRF matters for session-based auth where the browser auto-sends cookies
  • Safe to disable for stateless JWT APIs — no cookies, no CSRF risk
  • Spring Security uses the Synchronizer Token pattern (hidden form field) or the Cookie-to-Header pattern
  • SPA + cookie auth: use CookieCsrfTokenRepository.withHttpOnlyFalse() so JavaScript can read the token
  • Disable: http.csrf(csrf -> csrf.disable()) — only for truly stateless APIs

Spring Data & Transactions Both

Transactions

@Transactional Propagation Types (REQUIRED, REQUIRES_NEW, NESTED)

Propagation 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.

  • REQUIRED (default): joins existing txn or creates a new one — most common
  • REQUIRES_NEW: always creates a new txn, suspending the outer one — inner commits/rolls back independently
  • NESTED: creates a savepoint within the existing txn — inner rollback reverts to savepoint, not the whole txn
  • SUPPORTS: runs in a txn if one exists, otherwise non-transactional
  • NOT_SUPPORTED: suspends any existing txn and runs non-transactionally

Isolation Levels in Spring

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.

  • READ_UNCOMMITTED: allows dirty reads — rarely used
  • READ_COMMITTED: prevents dirty reads — default for PostgreSQL, Oracle
  • REPEATABLE_READ: prevents dirty and non-repeatable reads — default for MySQL InnoDB
  • SERIALIZABLE: full isolation, highest consistency, lowest concurrency
  • DEFAULT: uses the database's default — what Spring uses if you don't specify

LazyInitializationException — Why It Happens

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.

  • JPA lazy associations return a proxy; accessing it after the session closes throws the exception
  • Root cause: fetching data in a controller/view layer after the @Transactional service method has returned
  • Fixes: use EAGER fetch (bad for performance), JOIN FETCH in query, @EntityGraph, or DTO projections
  • Open Session in View (spring.jpa.open-in-view=true) keeps the session open during request processing — enabled by default but considered an anti-pattern
  • Best practice: fetch exactly what you need in the service layer with explicit queries

N+1 Problem & @EntityGraph

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.

  • N+1: 1 query for the parent list + N queries for each parent's children = N+1 total queries
  • JOIN FETCH: SELECT o FROM Order o JOIN FETCH o.items — loads everything in one query
  • @EntityGraph: declarative alternative — @EntityGraph(attributePaths = {"items"}) on repository method
  • @BatchSize: Hibernate fetches lazy collections in batches instead of one-by-one
  • Always verify with SQL logging: spring.jpa.show-sql=true or p6spy

JPQL vs Native Queries

JPQL 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.

  • JPQL: entity-centric, portable, supports JPA features (managed entities, lazy loading)
  • Native SQL: full SQL power (window functions, CTEs, hints) but results need manual mapping
  • Use @Query(nativeQuery = true) for native SQL in Spring Data repositories
  • Named parameters work in both: :paramName syntax
  • Trade-off: JPQL for most CRUD; native for complex analytics, bulk operations, or DB-specific features

Repositories

Custom Query Derivation

Spring 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.

  • Method name parsing: findBy + property path + operator keyword
  • Operators: And, Or, Between, LessThan, Like, In, OrderBy, etc.
  • Supports nested properties: findByAddress_City(String city)
  • Limitations: complex joins, subqueries, or grouping require @Query or Specifications
  • Spring Data verifies method names at startup — typos cause BeanCreationException

@Query with JPQL

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.

  • JPQL: @Query("SELECT u FROM User u WHERE u.email = :email")
  • Modifying queries: add @Modifying and @Transactional for UPDATE/DELETE
  • Supports SpEL: #{#entityName} resolves to the entity class name
  • Pagination works with @Query — add a Pageable parameter and a countQuery

Projections

Projections 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.

  • Interface projections: define an interface with getter methods matching entity properties — Spring generates the implementation
  • Class-based (DTO) projections: constructor expression in JPQL — SELECT new com.example.UserDTO(u.name, u.email) FROM User u
  • Dynamic projections: <T> List<T> findByStatus(String status, Class<T> type)
  • Open projections with @Value("#{target.firstName + ' ' + target.lastName}") — flexible but lose query optimization

Pagination & Sorting

Spring 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.

  • Pass Pageable (via PageRequest.of(page, size, sort)) to any repository method
  • Return Page<T> (includes total count) or Slice<T> (no count query — more efficient)
  • Sort: Sort.by(Sort.Direction.DESC, "createdAt") or via query derivation: findByStatusOrderByCreatedAtDesc
  • Web support: @PageableDefault(size=20, sort="id") in controller method parameters
  • Keyset pagination (WHERE id > :lastId) is more efficient than OFFSET for large datasets but needs manual implementation

Recommended Resources