← Home
Spring / Spring Boot

Top 8 Senior Interview Questions

IoC · AOP · Transactions · Auto-configuration · Security · Actuator

Questions

  1. Spring IoC and Dependency Injection — types and tradeoffs
  2. How @Transactional works — pitfalls and failure modes
  3. Spring AOP — how it works, use cases
  4. Spring Boot auto-configuration internals
  5. Bean lifecycle — @PostConstruct, @PreDestroy, scopes
  6. Spring Security filter chain
  7. @ConfigurationProperties vs @Value
  8. Graceful shutdown and health probes
Q1 · Core
Explain Spring IoC and Dependency Injection. Which injection type do you prefer and why?

IoC concept

The framework creates and manages object lifecycles. You declare what you need; the container provides it. DI is the mechanism — dependencies are injected rather than created by the class itself.

Three injection types

// 1. Constructor injection — PREFERRED
@Service
public class PaymentService {
    private final AccountRepository repo;   // final — immutable, testable
    private final FraudChecker fraud;

    public PaymentService(AccountRepository repo, FraudChecker fraud) {
        this.repo = repo;
        this.fraud = fraud;
    }
}

// 2. Setter injection — for optional dependencies only
@Autowired(required = false)
public void setMetricsCollector(MetricsCollector collector) { ... }

// 3. Field injection — avoid. Hides dependencies, can't use final, not testable without Spring
@Autowired private AccountRepository repo; // BAD

Why constructor injection wins

  • Dependencies are explicit — visible in the constructor signature.
  • Fields can be final → immutable bean, thread-safe.
  • Circular dependency fails fast at startup, not at runtime.
  • Unit tests don't need Spring: just new PaymentService(mockRepo, mockFraud).
Likely follow-up: "How do you resolve circular dependencies?" → Refactor to break the cycle. If unavoidable: use @Lazy on one dependency, or redesign using an event/observer pattern.
Q2 · Transactions
How does @Transactional work internally? What are the most common pitfalls?

Proxy mechanics

  • Spring wraps the bean in a proxy (JDK Dynamic Proxy if interface exists, CGLIB otherwise).
  • External call to @Transactional method → proxy intercepts → opens transaction → calls real method → commits or rolls back.
  • this.method() inside the same class bypasses the proxy entirely — no transaction.

Pitfall 1 — self-invocation

@Service
public class OrderService {
    public void processBatch(List<Order> orders) {
        orders.forEach(this::processOne); // BUG: 'this' bypasses proxy!
    }

    @Transactional
    public void processOne(Order o) { ... }
}

// Fix: extract to a separate bean, OR inject self:
@Autowired private OrderService self;
orders.forEach(self::processOne); // goes through proxy

Pitfall 2 — checked exceptions don't roll back

@Transactional  // default: rollback on RuntimeException only
public void transfer() throws BusinessException {
    debit();
    credit();
    throw new BusinessException("limit exceeded"); // does NOT roll back!
}

// Fix:
@Transactional(rollbackFor = BusinessException.class)

Pitfall 3 — REQUIRES_NEW deadlock

@Transactional
public void outer() {
    repo.lockRow(id);  // holds DB lock
    inner();           // opens NEW connection — same row, deadlock!
}

@Transactional(propagation = REQUIRES_NEW)
public void inner() {
    repo.readRow(id);  // waits for lock held by outer → deadlock
}

REQUIRES_NEW suspends the outer transaction and borrows a new DB connection. Two connections from the same thread fighting for the same row = deadlock.

Read-only optimisation

@Transactional(readOnly = true)
public List<Account> getAccounts() { ... }
// Hibernate skips dirty checking, can route to read replica, skips flush.
// Real performance impact at scale — not just documentation.
Likely follow-up: "Should you put @Transactional on the interface or class?" → Class (implementation). If Spring uses CGLIB instead of JDK proxy, annotations on interfaces are ignored.
Q3 · AOP
What is Spring AOP? How does it work and what are the practical use cases?

Core concepts

  • Aspect: module containing cross-cutting logic.
  • Pointcut: expression that matches join points (which methods to intercept).
  • Advice: the code to run — Before, After, Around, AfterReturning, AfterThrowing.
  • Join point: specific execution point (method call in Spring AOP).

Spring AOP uses proxies (not bytecode weaving like AspectJ). This means it only intercepts Spring-managed bean method calls — not constructor calls, field access, or private methods.

Around advice example — execution time logging

@Aspect
@Component
public class PerformanceAspect {

    @Around("@annotation(Monitored)")
    public Object measure(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return pjp.proceed();
        } finally {
            long elapsed = System.currentTimeMillis() - start;
            log.info("{} took {}ms", pjp.getSignature().getName(), elapsed);
        }
    }
}

Practical use cases

  • Audit logging: log who called what with what parameters — without polluting service code.
  • Security checks: @PreAuthorize in Spring Security is AOP under the hood.
  • Performance monitoring: method-level latency tracking for SLAs.
  • Retry logic: @Retryable from Spring Retry is AOP.
  • Caching: @Cacheable is AOP — intercepts, checks cache, calls method only on miss.
Limitation: Self-invocation doesn't work (same as @Transactional) — the proxy isn't involved. Also, AOP applies only to Spring-managed beans.
Likely follow-up: "What's the difference between Spring AOP and AspectJ?" → Spring AOP: proxy-based, runtime, only method execution. AspectJ: compile/load-time weaving, full language, can intercept constructors/fields — but much heavier setup.
Q4 · Spring Boot Internals
How does Spring Boot auto-configuration work under the hood?

The startup sequence

  1. @SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan.
  2. @EnableAutoConfiguration imports AutoConfigurationImportSelector.
  3. Selector reads META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (Boot 3+) — a list of hundreds of auto-config classes.
  4. Each class is evaluated with @Conditional annotations — only matching ones are loaded.

Conditional annotations

@AutoConfiguration
@ConditionalOnClass(DataSource.class)            // JDBC on classpath?
@ConditionalOnProperty("spring.datasource.url")  // URL configured?
public class DataSourceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean  // user didn't define their own DataSource?
    public DataSource dataSource(DataSourceProperties props) {
        return new HikariDataSource(...);
    }
}
  • @ConditionalOnMissingBean: your bean overrides the default. Auto-configs run after user @Configuration classes, so your beans are already registered when evaluated.
  • @ConditionalOnClass: add a dependency to your POM → behaviour changes automatically.

How to debug auto-configuration

# Run with --debug flag to see what matched and what didn't:
java -jar app.jar --debug

# Or check the conditions report via Actuator:
GET /actuator/conditions

Excluding an auto-config

@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
// Or in properties:
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
Likely follow-up: "What is the difference between spring.factories and AutoConfiguration.imports?" → Boot 3 moved to the new imports file for performance — scanning the imports file is faster than loading spring.factories. Old spring.factories still works but is deprecated for auto-configs.
Q5 · Bean Lifecycle
Explain the Spring Bean lifecycle. When do you use @PostConstruct and @PreDestroy?

Lifecycle order

Constructor
  → @Autowired dependencies injected
  → @PostConstruct
  → InitializingBean.afterPropertiesSet()
  → @Bean(initMethod)
  → ApplicationReadyEvent / ApplicationRunner
  ─── SERVING TRAFFIC ───
  → @PreDestroy
  → DisposableBean.destroy()
  → @Bean(destroyMethod)

@PostConstruct use cases

@PostConstruct
public void init() {
    // Validate required config (fail fast before serving traffic)
    if (props.getApiKey().isBlank()) throw new IllegalStateException("API key required");

    // Warm up caches on startup
    cache.load(configRepo.findAll());
}

@PreDestroy use cases

@PreDestroy
public void shutdown() {
    // Flush in-flight Kafka messages before JVM exits
    producer.flush();
    producer.close();

    // Drain in-progress jobs
    executor.shutdown();
    executor.awaitTermination(30, TimeUnit.SECONDS);
}

Bean scopes

ScopeInstancesUse for
singleton1 per contextStateless services (default)
prototypeNew per injectionStateful objects (report generators)
request1 per HTTP requestWeb-scoped state
session1 per HTTP sessionUser session data
Pitfall: Injecting a prototype bean into a singleton gives you effectively a singleton — the singleton holds the same prototype instance forever. Use ObjectFactory<T> or @Lookup to get a fresh instance each time.
Likely follow-up: "What's the difference between @PostConstruct and ApplicationRunner?" → @PostConstruct runs during bean initialisation (context refresh). ApplicationRunner/CommandLineRunner runs after the full context is up and the server is ready to serve traffic.
Q6 · Security
How does Spring Security work? Describe the filter chain and JWT integration.

Filter chain

Spring Security is a chain of Filters, registered as a DelegatingFilterProxy. Every HTTP request passes through the chain in order.

FilterPurpose
SecurityContextPersistenceFilterLoads/saves SecurityContext
CorsFilterCORS preflight handling
CsrfFilterCSRF token validation
UsernamePasswordAuthenticationFilterForm login
BearerTokenAuthenticationFilterJWT / OAuth2 bearer token
AuthorizationFilterRole/permission check
ExceptionTranslationFilterConverts security exceptions to HTTP responses

Modern SecurityFilterChain config

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .csrf(csrf -> csrf.disable())           // stateless API — no CSRF needed
        .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/public/**").permitAll()
            .requestMatchers("/api/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
        .build();
}

JWT flow

  1. Client sends Authorization: Bearer <token>.
  2. BearerTokenAuthenticationFilter extracts the token.
  3. JwtDecoder validates signature + expiry.
  4. Claims mapped to Authentication object → stored in SecurityContextHolder.
  5. Downstream code uses @PreAuthorize("hasRole('ADMIN')") or SecurityContextHolder.getContext().getAuthentication().
Likely follow-up: "What's the difference between authentication and authorisation?" → Authentication: who are you? (verify identity). Authorisation: what are you allowed to do? (check permissions). In Spring: AuthenticationManager handles the former; AccessDecisionManager / AuthorizationManager the latter.
Q7 · Configuration
@ConfigurationProperties vs @Value — what's the difference and when do you use each?

@Value — one property at a time

@Value("${payment.gateway.url}")     private String url;
@Value("${payment.gateway.timeout:5000}") private int timeout; // with default

@ConfigurationProperties — typed, grouped, validated

@ConfigurationProperties(prefix = "payment.gateway")
@Validated
public class GatewayProperties {
    @NotBlank private String url;
    @Min(100) private int timeout = 5000;
    @Min(1)   private int retries = 3;
    private Map<String, String> headers = new HashMap<>();
}

Comparison

Feature@Value@ConfigurationProperties
Type safetyNoYes — validated at startup
GroupingScatteredOne class per config group
Relaxed bindingNoYes — PAYMENT_GATEWAY_URL = payment.gateway.url
Maps/ListsDifficultNative support
IDE autocompleteNoYes (with metadata processor)
Rule: Use @Value for one-off injections. Use @ConfigurationProperties for anything with 3+ related properties or where validation matters.

Property resolution priority

1. Command line args       --server.port=9090
2. OS env vars             SERVER_PORT=9090
3. application-{profile}.yml
4. application.yml
→ Secrets go in env vars / Vault, never in yml files committed to git.
Likely follow-up: "How do you handle secrets?" → Never in source code. Use environment variables (injected by Kubernetes Secrets), or a secrets manager (HashiCorp Vault, AWS Secrets Manager) with Spring Cloud Vault integration.
Q8 · Production
How do you implement graceful shutdown and health probes in Spring Boot?

Graceful shutdown

# application.yml
server:
  shutdown: graceful                      # stop accepting new requests
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s       # max wait for in-flight

On SIGTERM: Spring stops accepting new requests, waits for in-flight requests to complete (up to timeout), then shuts down. For Kafka consumers, implement SmartLifecycle to stop consuming before the timeout.

Health probes for Kubernetes

# application.yml
management:
  endpoint:
    health:
      probes:
        enabled: true       # enables /actuator/health/liveness and /readiness
  health:
    livenessState:
      enabled: true
    readinessState:
      enabled: true
  • Liveness /actuator/health/liveness: "Is the process alive?" Fail → Kubernetes restarts the pod.
  • Readiness /actuator/health/readiness: "Can it serve traffic?" Fail → removed from load balancer (used during startup and graceful shutdown).

Custom health indicator

@Component
public class KafkaHealthIndicator implements HealthIndicator {
    @Override
    public Health health() {
        try {
            kafkaAdmin.describeCluster();
            return Health.up().withDetail("broker", "reachable").build();
        } catch (Exception e) {
            return Health.down().withDetail("error", e.getMessage()).build();
        }
    }
}
Tip: Don't include slow external dependency checks in your readiness probe — if the dependency is down, that doesn't mean you should be pulled from the load balancer. Keep probes fast (<1s).
Likely follow-up: "What metrics do you expose via Actuator?" → Micrometer auto-instruments JVM, thread pool, HTTP requests. Add custom business metrics: meterRegistry.counter("payments.processed"). Scraped by Prometheus, visualised in Grafana.