Skip to content

SPRING.md โ€” Spring Boot / Spring Framework Deep Study Guide โ€‹

๐Ÿ“ Quiz ยท ๐Ÿƒ Flashcards

Companion deep-dive for Section 2 of INTERVIEW_PREP.md. Target: senior/architect-level interview prep for Java/Spring backend roles. Anchored to realistic anchor examples โ€” a legacy MQ microservice, cross-team schema standardization, a JAXB migration, ArgoCD/K8s, and a multi-tenant productivity app.

How to Use This Guide โ€‹

Each section follows the same shape:

  1. Concept โ€” what the thing is, why it exists, how it fits into the broader Spring model.
  2. Canonical code โ€” minimal idiomatic snippet you could reproduce on a whiteboard.
  3. Interview Q&A โ€” questions you'll actually be asked, with compressed answer hints.
  4. Pitfalls โ€” the gotchas interviewers use to separate juniors from seniors.

Study tip: for every gotcha here, try to recall a real bug you hit or story you can tell. Concrete beats abstract every time.


Table of Contents โ€‹

  1. Spring Core & the IoC Container
  2. Dependency Injection
  3. Bean Lifecycle & Bean Post-Processors
  4. Spring Boot Fundamentals & Auto-Configuration
  5. Externalized Configuration & Profiles
  6. Spring MVC & REST
  7. Exception Handling & Validation
  8. Spring AOP & the Proxy Model
  9. Transactions (@Transactional Deep Dive)
  10. Spring Data (JPA + MongoDB)
  11. Spring Security
  12. Spring Boot Actuator & Observability
  13. Spring Boot Testing
  14. Spring WebFlux & Reactive
  15. Spring Cloud & Microservices Patterns
  16. Caching, Scheduling, Async
  17. Spring Boot 3.x Modern Features
  18. Messaging Integration (JMS, Kafka, AMQP)
  19. Performance, Startup & Tuning
  20. Common Scenario-Based Interview Questions
  21. Quick-Reference Cheat Sheet
  22. Further Reading & Official Docs

1. Spring Core & the IoC Container โ€‹

Concept โ€‹

Inversion of Control (IoC) means your application code no longer news up its collaborators โ€” the container does, and hands them to you. Dependency Injection (DI) is the specific IoC mechanism Spring uses: you declare what you need, Spring wires it. The payoff is loose coupling, easy substitution for tests, and centralized wiring.

Spring's IoC container is implemented by the BeanFactory interface and its richer descendant ApplicationContext. When people say "Spring container" in 2026, they mean ApplicationContext.

BeanFactoryApplicationContext
Bean instantiationLazy (on getBean())Eager (singletons at startup)
AOP auto-proxyingNo (manual)Yes
Event publicationNoYes (ApplicationEvent, @EventListener)
i18n (MessageSource)NoYes
Environment abstractionNoYes
Default in Spring BootNoYes

The eager instantiation is deliberate: fail-fast on configuration errors at startup, not at first request.

Concrete ApplicationContext implementations:

  • AnnotationConfigApplicationContext โ€” Java config (@Configuration).
  • GenericApplicationContext โ€” generic, used as base by Spring Boot.
  • AnnotationConfigServletWebServerApplicationContext โ€” what Spring Boot uses for web apps.
  • (Legacy) ClassPathXmlApplicationContext โ€” XML.

Bean definitions โ€‹

Three ways to register a bean:

java
// 1. Component scanning + stereotype annotations
@Service
public class HabitService { ... }

// 2. @Configuration + @Bean (for third-party classes you can't annotate)
@Configuration
public class MessagingConfig {
    @Bean
    public JmsTemplate jmsTemplate(ConnectionFactory cf) {
        return new JmsTemplate(cf);
    }
}

// 3. XML (legacy โ€” you'll see it only in old apps)

Stereotype annotations โ€‹

All four are functionally @Component at runtime, but they communicate intent and some carry extra behavior:

  • @Component โ€” generic Spring-managed bean.
  • @Service โ€” business logic layer. No runtime behavior, purely semantic.
  • @Repository โ€” persistence layer. Adds PersistenceExceptionTranslationPostProcessor which translates vendor-specific SQLExceptions into Spring's DataAccessException hierarchy.
  • @Controller โ€” Spring MVC controller (produces views).
  • @RestController โ€” @Controller + @ResponseBody (produces JSON/XML bodies).

Bean scopes โ€‹

ScopeWhen createdUse case
singleton (default)Once per containerStateless services, DAOs
prototypeEvery getBean() callStateful helpers you want fresh each call
requestPer HTTP requestRequest-scoped state
sessionPer HTTP sessionUser session cache
applicationPer ServletContextStatic config per app
websocketPer WebSocket sessionWS connection state

Injecting a prototype into a singleton is a trap โ€” the singleton is wired once, so it keeps the same "prototype" instance forever. Fix with scoped proxy:

java
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class JobContext { ... }

Interview Q&A โ€‹

  • What is IoC and why does Spring use it? Inversion of Control moves object construction/wiring out of your code into the container. Benefits: loose coupling, testability, declarative configuration, lifecycle management.
  • BeanFactory vs ApplicationContext? ApplicationContext is the enterprise superset โ€” eager init, events, i18n, AOP, Environment. Always pick it unless you're in a memory-pinched embedded scenario.
  • Do the four stereotype annotations differ? Semantically yes, at runtime they're all @Component. @Repository adds DB exception translation. That's the one real behavioral difference.
  • What happens if you inject a prototype bean into a singleton? You get one instance, held forever โ€” defeats the prototype scope. Fix with @Scope(proxyMode=TARGET_CLASS) or ObjectProvider<T> / Provider<T>.

Pitfalls โ€‹

  • Mutable state in a singleton is a data race waiting to happen. Stateless-by-default is the safe rule.
  • Prototype misuse โ€” people reach for it when they actually need a new-ed POJO.
  • Component-scan collisions โ€” two classes with the same bean name (e.g. UserService) from different packages โ†’ ConflictingBeanDefinitionException. Rename or qualify.
  • Forgetting that @Bean methods inside a @Configuration class go through CGLIB so inter-method calls are intercepted (unlike plain @Component, see ยง8).

2. Dependency Injection โ€‹

Concept โ€‹

Three DI styles exist; Spring recommends exactly one.

java
// โœ… CONSTRUCTOR INJECTION (recommended)
@Service
public class HabitService {
    private final HabitRepository repo;
    private final Clock clock;

    public HabitService(HabitRepository repo, Clock clock) {
        this.repo = repo;
        this.clock = clock;
    }
}
java
// โš ๏ธ SETTER INJECTION โ€” optional deps, circular-dep escape hatch
@Service
public class HabitService {
    private HabitRepository repo;

    @Autowired
    public void setRepo(HabitRepository repo) { this.repo = repo; }
}
java
// โŒ FIELD INJECTION (avoid)
@Service
public class HabitService {
    @Autowired private HabitRepository repo;
}

Why constructor wins:

  1. Immutability โ€” final fields, set once, thread-safe by construction.
  2. Required deps are obvious โ€” can't instantiate without them; compiler helps you.
  3. Testable without Spring โ€” new HabitService(mockRepo, Clock.systemUTC()) in a plain JUnit test.
  4. Circular deps fail loudly at startup rather than being silently resolved and biting you later.
  5. No reflection gymnastics in tests (ReflectionTestUtils.setField is a smell).

Since Spring 4.3, @Autowired on a constructor is optional if there's only one constructor โ€” modern code omits it.

Variants of @Autowired โ€‹

AnnotationOriginQuirk
@AutowiredSpringBy-type injection
@InjectJSR-330 (jakarta.inject)Same as @Autowired but portable
@ResourceJSR-250 (jakarta.annotation)By-name first, then by-type

Disambiguation โ€‹

When multiple candidates match a type:

java
@Bean @Primary
public DataSource primaryDataSource() { ... }

@Bean
@Qualifier("reporting")
public DataSource reportingDataSource() { ... }

@Service
public class Reports {
    public Reports(@Qualifier("reporting") DataSource ds) { ... }
}

Collection injection โ€‹

Inject all beans of a type โ€” powerful for strategy/chain-of-responsibility patterns:

java
@Service
public class NotificationDispatcher {
    private final List<NotificationChannel> channels;  // all beans of this type, ordered by @Order

    public NotificationDispatcher(List<NotificationChannel> channels) {
        this.channels = channels;
    }
}

Map<String, Channel> injects keyed by bean name โ€” useful for dispatch-by-key.

Circular dependencies โ€‹

A โ†’ B โ†’ A

Constructor injection + singleton + singleton = startup failure (BeanCurrentlyInCreationException). Spring can't build either bean without the other.

Setter/field injection can be resolved via Spring's early-reference mechanism (bean A is partially built, exposed to B, finished after). Spring Boot 2.6+ disables circular references by default โ€” you now have to opt back in (spring.main.allow-circular-references=true) but you shouldn't.

Fixes, in order of preference:

  1. Redesign โ€” extract shared logic into a third bean. Circular deps are almost always a hint that two classes have overlapping responsibilities.
  2. @Lazy on one side โ€” breaks the cycle by proxying that dep.
  3. Setter injection on one side โ€” works but hides the design problem.

Interview Q&A โ€‹

  • Why is constructor injection preferred? Immutability, required deps visible, testable without Spring, circular deps fail at startup.
  • How does Spring resolve List<Handler>? Finds all beans of type Handler, orders by @Order / Ordered / @Priority, injects.
  • How do you handle two beans of the same type? @Primary for the default, @Qualifier("name") to pick explicitly.
  • Circular deps โ€” how does Spring handle them and how do you avoid them? With constructor injection it fails at startup; with setter it resolves via early reference. Better: redesign or introduce a mediator bean.

Pitfalls โ€‹

  • Field injection breaks testability โ€” can only be set with reflection, masks required deps.
  • @Autowired(required=false) with field injection + missing bean = silent null. Prefer Optional<Bean> or ObjectProvider<Bean>.
  • @Resource name semantics โ€” bites people who assume it's just @Autowired with a different spelling.
  • Spring Boot 2.6+ circular deps disabled by default โ€” upgrading can reveal latent cycles.

3. Bean Lifecycle & Bean Post-Processors โ€‹

Concept โ€‹

Understanding this lifecycle is the senior-level IoC question because it underpins AOP, transactions, @PostConstruct ordering issues, and graceful shutdown.

Full sequence โ€‹

For a singleton bean:

1. Container loads BeanDefinition (from @ComponentScan, @Bean, XML, etc.)
2. BeanFactoryPostProcessor runs โ€” can mutate BeanDefinitions (not instances yet)
   e.g., PropertySourcesPlaceholderConfigurer resolves ${...}
3. Instantiate the bean (constructor)
4. Populate properties (setter/field injection; constructor deps were already set in step 3)
5. *Aware callbacks* in order:
     - BeanNameAware.setBeanName
     - BeanClassLoaderAware.setBeanClassLoader
     - BeanFactoryAware.setBeanFactory
     - EnvironmentAware, ResourceLoaderAware, ApplicationContextAware, etc.
6. BeanPostProcessor.postProcessBeforeInitialization
7. @PostConstruct method
8. InitializingBean.afterPropertiesSet()
9. Custom init-method (from @Bean(initMethod=...) or XML)
10. BeanPostProcessor.postProcessAfterInitialization   โ† AOP proxies wrap the bean HERE
11. Bean is ready โ€” injected into consumers
...
12. Application shutdown:
13. @PreDestroy method
14. DisposableBean.destroy()
15. Custom destroy-method

BeanPostProcessor vs BeanFactoryPostProcessor โ€‹

BeanFactoryPostProcessorBeanPostProcessor
When it firesBefore any bean is instantiatedOn every bean, around initialization
Operates onBeanDefinition metadataActual bean instances
Typical useResolve ${placeholders}, mutate configAOP proxy wrapping, annotation processing
ExamplePropertySourcesPlaceholderConfigurerAnnotationAwareAspectJAutoProxyCreator, CommonAnnotationBeanPostProcessor (handles @PostConstruct)

Why AOP happens in step 10 โ€‹

When AnnotationAwareAspectJAutoProxyCreator.postProcessAfterInitialization() sees a bean matching an aspect's pointcut, it replaces the bean reference in the container with a proxy. The original instance is wrapped inside the proxy. This is why self-invocation bypasses @Transactional (ยง8) โ€” the original instance calls its own methods directly without going through the proxy.

Canonical code โ€” lifecycle callbacks โ€‹

java
@Component
public class CacheWarmer implements InitializingBean, DisposableBean {

    @PostConstruct
    public void postConstruct() {
        // Runs first (recommended โ€” portable, no Spring interface needed)
    }

    @Override
    public void afterPropertiesSet() {
        // Runs second โ€” avoid unless you have a reason
    }

    @Override
    public void destroy() {
        // Runs before custom destroy method
    }
}

SmartLifecycle for ordered start/stop โ€‹

When you need coarser-grained control than per-bean callbacks (e.g., start message consumers after the web server is ready, stop them before the DB connection pool closes):

java
@Component
public class MqConsumerBootstrap implements SmartLifecycle {
    @Override public boolean isAutoStartup() { return true; }
    @Override public int getPhase() { return Integer.MAX_VALUE - 10; }  // start late, stop early
    @Override public void start() { ... }
    @Override public void stop() { ... }
    @Override public boolean isRunning() { ... }
}

This matters in a legacy MQ microservice โ€” the listener container should not start pulling messages until the DB pool and schema migration are confirmed ready.

Interview Q&A โ€‹

  • Walk through the Spring bean lifecycle. See the 15-step list above. Hit constructor โ†’ DI โ†’ aware โ†’ post-processors around init callbacks โ†’ in-use โ†’ destroy.
  • When would you write a BeanPostProcessor? Rare in app code โ€” common in frameworks (AOP, validation, @Async). Use case: generic behavior to layer on every bean matching a type (e.g., auto-register any MessageListener with a central registry).
  • Difference between BeanPostProcessor and BeanFactoryPostProcessor? The factory version runs earlier (before instantiation, mutates definitions). The bean version wraps/modifies actual instances.
  • Why does AOP not work in @PostConstruct? Because the proxy hasn't been applied yet at that point โ€” @PostConstruct runs in step 7, proxy wrapping happens in step 10. If you call a @Transactional method from @PostConstruct on this, no transaction.

Pitfalls โ€‹

  • Heavy work in constructors โ€” a slow DB ping in a constructor delays startup; put it in @PostConstruct or ApplicationReadyEvent.
  • @PostConstruct + self-invocation + @Transactional โ€” fails silently because the proxy isn't installed yet and self-invocation bypasses the proxy anyway.
  • Depending on injection order at @PostConstruct time โ€” all deps are set by step 4, so this is usually fine, but if a dep is another bean's @PostConstruct side effect, you need @DependsOn or event listening.
  • Shutdown ordering โ€” long-running @PreDestroy can block JVM shutdown beyond management.endpoint.shutdown.enabled grace period. Use SmartLifecycle.stop(Runnable callback) for async.

4. Spring Boot Fundamentals & Auto-Configuration โ€‹

Concept โ€‹

Spring Boot is Spring Framework + "opinionated defaults, zero-XML, runnable jar." The magic is auto-configuration: based on what's on the classpath and what beans you've already defined, Spring Boot configures the rest.

@SpringBootApplication composition โ€‹

java
@SpringBootApplication
// equivalent to:
@Configuration
@EnableAutoConfiguration
@ComponentScan(basePackages = "com.example.app")  // package of the annotated class
public class MyApp { ... }
  • @Configuration โ€” the class itself is a bean-definition source.
  • @EnableAutoConfiguration โ€” trigger auto-config discovery.
  • @ComponentScan โ€” scan the app's package (and subpackages) for stereotypes.

How auto-config is discovered โ€‹

  1. Spring Boot looks in every jar on the classpath for META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (Boot 2.7+; older versions used META-INF/spring.factories keyed by EnableAutoConfiguration).
  2. Each line in that file is a fully qualified class name of an auto-config class, e.g.:
    org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration
    org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration
  3. Each auto-config class is a @Configuration annotated with a cocktail of @ConditionalOn* โ€” it only applies when its conditions pass.

Conditional annotations โ€‹

AnnotationApplies when
@ConditionalOnClassClass is on the classpath
@ConditionalOnMissingClassClass is NOT on the classpath
@ConditionalOnBeanA bean of given type/name exists
@ConditionalOnMissingBeanThe user hasn't already defined one
@ConditionalOnPropertyA property is present (and optionally equals a value)
@ConditionalOnResourceA classpath/filesystem resource exists
@ConditionalOnWebApplicationRunning as a web app
@ConditionalOnNotWebApplicationNOT a web app
@ConditionalOnExpressionSpEL expression is true
@ConditionalOnJavaRunning on a specific Java version
@ConditionalOnCloudPlatformCloud Foundry, Kubernetes, Heroku

The @ConditionalOnMissingBean pattern is what lets you override Boot defaults by just defining your own bean. Example:

java
@Configuration
@ConditionalOnClass(JmsTemplate.class)
public class JmsAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public JmsTemplate jmsTemplate(ConnectionFactory cf) {
        return new JmsTemplate(cf);  // applied only if user hasn't defined their own
    }
}

In a typical legacy MQ microservice, you'd override jmsTemplate if you needed a custom MessageConverter or transaction manager โ€” just define your own @Bean JmsTemplate, and Boot's default drops out.

Auto-config ordering โ€‹

java
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
@ConditionalOnClass(EntityManager.class)
public class JpaAutoConfiguration { ... }

@AutoConfigureAfter, @AutoConfigureBefore, @AutoConfigureOrder control the relative order.

Writing your own starter โ€‹

Convention: two modules.

acme-spring-boot-autoconfigure/     # the @AutoConfiguration classes
  โ””โ”€ src/main/resources/META-INF/spring/
       org.springframework.boot.autoconfigure.AutoConfiguration.imports

acme-spring-boot-starter/           # an empty POM that depends on -autoconfigure
                                    # + pulls in required runtime deps

Users add the starter; they get auto-config transparently.

SpringApplication class โ€‹

java
public static void main(String[] args) {
    SpringApplication app = new SpringApplication(MyApp.class);
    app.setBannerMode(Banner.Mode.OFF);
    app.setDefaultProperties(Map.of("spring.main.lazy-initialization", "true"));
    app.addListeners(new MyListener());
    app.run(args);
}
  • Reads arguments, prints the banner, loads profiles.
  • Publishes lifecycle events: ApplicationStartingEvent โ†’ ApplicationEnvironmentPreparedEvent โ†’ ApplicationContextInitializedEvent โ†’ ApplicationPreparedEvent โ†’ ApplicationStartedEvent โ†’ ApplicationReadyEvent โ†’ ApplicationFailedEvent.
  • Runs ApplicationRunner / CommandLineRunner beans after refresh.
  • Installs FailureAnalyzers โ€” the pretty "Port 8080 is in use" error comes from PortInUseFailureAnalyzer.

Debugging auto-config โ€‹

  • Run with --debug โ†’ dumps the ConditionEvaluationReport (which auto-configs matched, which didn't, why).
  • Actuator endpoint /actuator/conditions โ€” same info at runtime.
  • @ImportAutoConfiguration(MyThing.class) in a test to load just one.

Interview Q&A โ€‹

  • Explain how Spring Boot auto-configuration works. Classpath scanning finds AutoConfiguration.imports files; each auto-config class is gated by @ConditionalOn*; your own @Bean definitions win via @ConditionalOnMissingBean; discovery is handled by @EnableAutoConfiguration.
  • What is @SpringBootApplication composed of? @SpringBootConfiguration (specialization of @Configuration) + @EnableAutoConfiguration + @ComponentScan.
  • How do you disable a specific auto-config? @SpringBootApplication(exclude = SecurityAutoConfiguration.class) or the property spring.autoconfigure.exclude.
  • Walk through writing a custom starter. Two modules: -autoconfigure with @AutoConfiguration classes and the imports file; -starter as a thin aggregator pulling in runtime deps.

Pitfalls โ€‹

  • @ConditionalOnMissingBean is a double-edged sword โ€” a misplaced user bean silently disables Boot defaults.
  • @ComponentScan default is the annotated class's package. Put your main class at the top of your package tree or you'll miss beans.
  • Classpath-only conditions โ€” a test that drags in extra jars can accidentally flip conditions, turning on unwanted beans.
  • Legacy spring.factories โ€” Boot 2.7+ moved auto-config to AutoConfiguration.imports; still supports the old file for compatibility, but new starters should use the new mechanism.

5. Externalized Configuration & Profiles โ€‹

Concept โ€‹

Spring Boot's config system resolves properties from many sources, in a deterministic precedence order. Higher precedence wins.

Property source precedence (top wins) โ€‹

  1. DevTools global settings (~/.config/spring-boot/)
  2. @TestPropertySource / test-specific
  3. Command-line args (--server.port=9090)
  4. SPRING_APPLICATION_JSON env var (inline JSON)
  5. ServletConfig init params
  6. ServletContext init params
  7. JNDI attributes (java:comp/env)
  8. Java system properties (-Dkey=val)
  9. OS environment variables
  10. Profile-specific application-{profile}.yml outside the jar
  11. Profile-specific application-{profile}.yml inside the jar
  12. application.yml outside the jar
  13. application.yml inside the jar
  14. @PropertySource on @Configuration
  15. Default properties (SpringApplication.setDefaultProperties)

@Value vs @ConfigurationProperties vs Environment โ€‹

java
// 1. @Value โ€” one-off, OK for simple scalars
@Value("${app.retry.max-attempts:3}")
private int maxAttempts;

// 2. @ConfigurationProperties โ€” type-safe, structured, validated
@ConfigurationProperties(prefix = "app.retry")
@Validated
public record RetryProps(
    @Min(1) int maxAttempts,
    @DurationMin(seconds = 1) Duration backoff,
    @NotEmpty List<Integer> retryableStatuses
) {}

@Configuration
@EnableConfigurationProperties(RetryProps.class)
class Config {}

// Inject it:
@Service
class ApiClient {
    ApiClient(RetryProps props) { ... }
}

// 3. Environment โ€” imperative access when you need computed keys
@Autowired Environment env;
String val = env.getProperty("dynamic.key." + tenantId, String.class);

When to pick each:

Use caseTool
Single scalar, no reuse@Value
Grouped config, type-safe, validated@ConfigurationProperties
Dynamic/computed key lookupEnvironment

@Value gotchas: no default + missing prop = startup failure; SpEL support (#{...}) is powerful but confusing; not refreshable.

Relaxed binding โ€‹

@ConfigurationProperties uses relaxed binding โ€” these are all equivalent:

app.retry.max-attempts
app.retry.maxAttempts
APP_RETRY_MAX_ATTEMPTS
app_retry_max_attempts

This is why env-var-driven config works so cleanly in Docker/Kubernetes.

Profiles โ€‹

yaml
# application.yml
spring:
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:dev}
    group:
      prod: prod-db, prod-secrets, prod-observability

---
spring:
  config:
    activate:
      on-profile: prod
mongodb:
  uri: ${MONGO_URI}
java
@Service
@Profile("!test")  // everything except test
public class RealEmailSender implements EmailSender { ... }

@Service
@Profile("test")
public class NoopEmailSender implements EmailSender { ... }

Config trees (Kubernetes-native) โ€‹

yaml
spring:
  config:
    import: configtree:/etc/config/

With a ConfigMap mounted at /etc/config/ where each file is a property, Spring Boot picks them up automatically โ€” no custom glue code.

@RefreshScope (Spring Cloud) โ€‹

java
@RefreshScope
@Component
public class FeatureFlags {
    @Value("${feature.new-rule-engine:false}")
    private boolean newRuleEngine;
}

Combined with Spring Cloud Config Server + Bus, POST to /actuator/refresh and the bean is rebuilt with new props โ€” no redeploy.

Interview Q&A โ€‹

  • What's the precedence of property sources? CLI args โ†’ env vars โ†’ profile-specific outside-jar โ†’ profile-specific inside-jar โ†’ generic outside-jar โ†’ generic inside-jar โ†’ defaults. (Give 3โ€“4 from the top; nobody expects all 15.)
  • @Value vs @ConfigurationProperties? @Value for one-off scalars, @ConfigurationProperties for grouped, validated, type-safe config โ€” always pick @ConfigurationProperties for anything non-trivial.
  • How do you handle secrets in prod? External secret stores (Vault, AWS Secrets Manager, K8s External Secrets Operator, Sealed Secrets), injected via env var or config tree. Never commit secrets to application.yml.
  • Profile groups โ€” what problem do they solve? One logical profile (prod) activates several sub-profiles (prod-db, prod-secrets, prod-observability). Keeps profile files focused.

Pitfalls โ€‹

  • @Value without a default + missing property = IllegalArgumentException at startup. Always default or wrap in Optional semantics.
  • Profile-specific files don't inherit across profiles โ€” application-dev.yml doesn't "extend" application.yml in any inheritance sense; both are merged into the Environment, with profile-specific winning key-by-key.
  • Boot 2.4+ changed profile-specific loading โ€” multi-document YAML with spring.config.activate.on-profile replaces the older convention. Don't mix styles.
  • @ConfigurationProperties on records โ€” requires constructor binding, and all fields present (no partial binding like setter-based).

6. Spring MVC & REST โ€‹

Concept โ€‹

Spring MVC is Spring's servlet-based web framework. Even in a "reactive world," 95% of Spring REST backends use it โ€” augmented in 2026 with virtual threads (ยง17).

Request lifecycle (the money diagram) โ€‹

HTTP request
    โ†“
FilterChain (servlet filters โ€” Spring Security lives here)
    โ†“
DispatcherServlet.doDispatch()
    โ†“
HandlerMapping.getHandler(request)  โ† finds the @Controller method
    โ†“
HandlerInterceptor.preHandle()       โ† your interceptors
    โ†“
HandlerAdapter.handle()              โ† invokes the controller method
    โ†“
    โ”œโ”€โ”€ Argument resolvers: @PathVariable, @RequestParam, @RequestBody, @RequestHeader, etc.
    โ”‚       HttpMessageConverter reads the body (JSON โ†’ POJO)
    โ”œโ”€โ”€ @Controller method executes
    โ””โ”€โ”€ Return-value handlers: writes response via HttpMessageConverter
    โ†“
HandlerInterceptor.postHandle()      โ† (MVC only โ€” runs if no exception)
    โ†“
ViewResolver (if returning a view name) OR direct body write
    โ†“
HandlerInterceptor.afterCompletion() โ† always runs
    โ†“
HTTP response

If anything throws, HandlerExceptionResolver (@ControllerAdvice + @ExceptionHandler, ResponseStatusExceptionResolver, etc.) kicks in.

Controllers โ€‹

java
@RestController
@RequestMapping("/api/habits")
@RequiredArgsConstructor  // Lombok โ€” generates constructor for final fields
public class HabitController {

    private final HabitService habitService;

    @GetMapping
    public Page<HabitDto> list(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return habitService.list(PageRequest.of(page, size));
    }

    @GetMapping("/{id}")
    public HabitDto get(@PathVariable UUID id) {
        return habitService.findById(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public HabitDto create(@RequestBody @Valid CreateHabitRequest req) {
        return habitService.create(req);
    }

    @PutMapping("/{id}")
    public HabitDto update(@PathVariable UUID id, @RequestBody @Valid UpdateHabitRequest req) {
        return habitService.update(id, req);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable UUID id) {
        habitService.delete(id);
    }
}

HttpMessageConverter โ€‹

By default:

  • MappingJackson2HttpMessageConverter โ€” JSON (Jackson).
  • Jaxb2RootElementHttpMessageConverter โ€” XML via JAXB. This is where a Thymeleafโ†’JAXB migration plugs in โ€” once your classes are @XmlRootElement-annotated, Spring serializes them natively. No template strings, no encoding bugs.
  • StringHttpMessageConverter โ€” plain text.
  • ByteArrayHttpMessageConverter โ€” raw bytes.
  • FormHttpMessageConverter โ€” application/x-www-form-urlencoded.

To add Avro/Protobuf, register your own converter or use spring-cloud-stream for Kafka.

Content negotiation โ€‹

Spring picks the converter by matching the request Accept header to converter-supported media types, refined by the controller's produces:

java
@GetMapping(value = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
public Habit get(@PathVariable UUID id) { ... }

HandlerInterceptor vs Filter vs @ControllerAdvice โ€‹

FilterHandlerInterceptor@ControllerAdvice
Runs atServlet container level, before DispatcherServletInside DispatcherServlet, around handlerAfter the handler throws or a cross-cutting response customization
SeesRaw HttpServletRequest/ResponseHandlerMethod, model, viewMethod args, thrown exceptions
Typical useAuth (Spring Security), CORS, request loggingMetric timing, auditing, tenant resolutionGlobal exception mapping, response body advice

Pagination โ€‹

java
@GetMapping
public Page<HabitDto> list(@PageableDefault(size = 20) Pageable pageable) {
    return repo.findAll(pageable).map(this::toDto);
}

Pageable auto-binds from ?page=0&size=20&sort=createdAt,desc. Returns HAL-style links when using Spring HATEOAS or Spring Data REST.

REST design principles โ€‹

  • Resource URIs are nouns, not verbs: /orders/{id}, not /getOrder.
  • HTTP verbs carry the action: GET read, POST create, PUT replace, PATCH partial update, DELETE remove.
  • Idempotent methods: GET, PUT, DELETE, HEAD, OPTIONS. POST is not (design idempotency keys if needed).
  • Status codes matter: 200 OK, 201 Created + Location, 202 Accepted (async), 204 No Content (DELETE), 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict (version conflict), 422 Unprocessable Entity (validation), 500 Internal Server Error.

Interview Q&A โ€‹

  • Walk through the Spring MVC request lifecycle. Filter chain โ†’ DispatcherServlet โ†’ HandlerMapping โ†’ Interceptor preHandle โ†’ HandlerAdapter โ†’ argument resolvers + message converters โ†’ controller method โ†’ return-value handlers โ†’ postHandle โ†’ afterCompletion.
  • @RestController vs @Controller? @RestController = @Controller + @ResponseBody on every method โ€” for JSON/XML APIs. @Controller returns view names resolved by ViewResolver.
  • How does Spring pick a JSON converter? By matching the request/response media types against registered HttpMessageConverters, refined by controller consumes/produces attributes.
  • Filter vs Interceptor vs ControllerAdvice? Filters run at the servlet layer (auth, logging); Interceptors run inside the DispatcherServlet around the handler; ControllerAdvice is for exception handling and response body manipulation.

Pitfalls โ€‹

  • Default Jackson config serializes all non-null fields โ€” you probably want @JsonInclude(NON_NULL) globally to avoid leaking nulls.
  • Confusion between @RequestParam and @PathVariable โ€” the former is from query string, the latter from the URI template.
  • CORS pre-flight โ€” OPTIONS requests need to be permitted without auth; Spring Security's cors() DSL handles this if wired correctly.
  • Jackson polymorphic deserialization (@JsonTypeInfo) is a classic deserialization CVE vector โ€” lock the allowed type hierarchy.

7. Exception Handling & Validation โ€‹

Concept โ€‹

Goal: a single consistent error response shape across your whole API, with validation errors being structured (not just "Bad Request").

Global exception handling โ€‹

java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(EntityNotFoundException.class)
    public ProblemDetail notFound(EntityNotFoundException ex) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ProblemDetail validation(MethodArgumentNotValidException ex) {
        ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
        pd.setTitle("Validation failed");
        pd.setProperty("errors", ex.getBindingResult().getFieldErrors().stream()
                .map(fe -> Map.of("field", fe.getField(), "message", fe.getDefaultMessage()))
                .toList());
        return pd;
    }

    @ExceptionHandler(Exception.class)
    public ProblemDetail generic(Exception ex) {
        log.error("Unhandled exception", ex);
        return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error");
    }
}

ProblemDetail โ€” RFC 7807 โ€‹

Boot 3 standardizes API error responses on RFC 7807. Enable globally:

yaml
spring.mvc.problemdetails.enabled: true

Now Spring's built-in exceptions (NoHandlerFoundException, HttpRequestMethodNotSupportedException, etc.) produce responses like:

json
{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "No endpoint GET /api/habits/xyz.",
  "instance": "/api/habits/xyz"
}

You can also extend ErrorResponseException for custom typed errors:

java
public class HabitNotFoundException extends ErrorResponseException {
    public HabitNotFoundException(UUID id) {
        super(HttpStatus.NOT_FOUND,
              ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, "Habit " + id + " not found"),
              null);
    }
}

Bean Validation โ€‹

java
public record CreateHabitRequest(
    @NotBlank @Size(max = 100) String name,
    @Size(max = 500) String description,
    @NotNull Frequency frequency,
    @FutureOrPresent LocalDate startDate
) {}

@PostMapping
public HabitDto create(@RequestBody @Valid CreateHabitRequest req) { ... }
  • @Valid triggers validation on the argument.
  • Failures throw MethodArgumentNotValidException โ†’ handled in the advice above.

Service-layer validation (@Validated) โ€‹

@Valid only works on controller args by default. For service methods:

java
@Service
@Validated  // enables method-level validation for this bean
public class HabitService {

    public Habit rename(@NotNull UUID id, @NotBlank String newName) { ... }
}

Invalid args now throw ConstraintViolationException (different exception class โ€” handle both).

BindingResult (old school but still shows up) โ€‹

java
@PostMapping
public ResponseEntity<?> create(@RequestBody @Valid Req req, BindingResult br) {
    if (br.hasErrors()) { ... }  // bypasses the advisor; error-handling lives in the controller
}

Don't do this unless you have a specific reason โ€” global handling via advice is cleaner.

Interview Q&A โ€‹

  • How do you handle exceptions globally in Spring? @RestControllerAdvice with @ExceptionHandler methods mapping exception types to ResponseEntity or ProblemDetail.
  • What is ProblemDetail? RFC 7807 implementation in Spring 6 / Boot 3 โ€” standard machine-readable error format for HTTP APIs.
  • @Valid vs @Validated? @Valid (JSR-303) triggers validation on controller args; @Validated (Spring) enables method-level validation on any bean and supports validation groups.
  • What's the difference between MethodArgumentNotValidException and ConstraintViolationException? The first comes from @Valid on @RequestBody/controller args; the second from @Validated on service methods. Both need handlers.

Pitfalls โ€‹

  • @ControllerAdvice ordering โ€” if you have multiple advice beans, they can compete. Use @Order or be very narrow in basePackages / assignableTypes.
  • Service-layer validation not firing โ€” you need @Validated on the class, and the class must be a Spring-managed bean (proxies again, ยง8).
  • Re-throwing loses the root cause โ€” always pass cause through when wrapping, or downstream logs look like magic.
  • Boot 3 changed default HTTP status mappings โ€” migration gotcha if you relied on specific response bodies from Spring's built-in exceptions.

8. Spring AOP & the Proxy Model โ€‹

Concept โ€‹

AOP (Aspect-Oriented Programming) cleanly separates cross-cutting concerns โ€” logging, security, transactions, caching, metrics โ€” from business logic. Spring's AOP is proxy-based, which has important implications.

Vocabulary โ€‹

  • Aspect โ€” a cross-cutting concern (the @Aspect class).
  • Join point โ€” a point in execution where advice can be applied (in Spring: method execution only).
  • Pointcut โ€” an expression selecting joint points.
  • Advice โ€” the code to run at a join point (@Before, @After, @Around, etc.).
  • Weaving โ€” linking aspects to target code. Spring weaves at runtime via proxies; AspectJ can weave at compile or load time.

Advice types โ€‹

java
@Aspect
@Component
public class LoggingAspect {

    @Pointcut("@annotation(com.acme.Audited)")
    void audited() {}

    @Before("audited()")
    public void logBefore(JoinPoint jp) { ... }

    @After("audited()")
    public void logAfter(JoinPoint jp) { ... }  // finally-like

    @AfterReturning(value = "audited()", returning = "result")
    public void logReturn(Object result) { ... }

    @AfterThrowing(value = "audited()", throwing = "ex")
    public void logThrow(Throwable ex) { ... }

    @Around("audited()")
    public Object time(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        try {
            return pjp.proceed();  // invoke the target
        } finally {
            long elapsed = System.nanoTime() - start;
            metrics.record(pjp.getSignature().toShortString(), elapsed);
        }
    }
}

Pointcut expression vocabulary โ€‹

ExpressionMatches
execution(* com.acme.service.*.*(..))Any method in com.acme.service package
within(com.acme.web..*)Any method in web package (and subpackages)
@annotation(org.springframework.transaction.annotation.Transactional)Methods annotated @Transactional
@within(org.springframework.stereotype.Service)Methods of classes annotated @Service
bean(habitService)The specific bean by name (Spring-specific, non-AspectJ)
args(..)Methods whose args match a type signature

Combine with &&, ||, !.

JDK dynamic proxy vs CGLIB โ€‹

JDK dynamic proxyCGLIB
HowGenerates a proxy class that implements the target's interfacesGenerates a subclass of the target
RequiresTarget must implement an interfaceTarget class must not be final
Instance typeThe interface onlyThe concrete class
Default in Boot 2.0+Only if target has an interface and you opted outDefault โ€” works even without interfaces

You'll see both in the wild. Spring Boot 2.0+ defaults to CGLIB because most Boot apps don't bother with service interfaces, and it works uniformly.

The big gotcha: self-invocation โ€‹

java
@Service
public class OrderService {

    public void placeOrder(Order o) {
        validate(o);
        saveAndNotify(o);  // โ† THIS CALL BYPASSES THE PROXY
    }

    @Transactional
    public void saveAndNotify(Order o) { ... }  // no transaction!
}

Why? placeOrder() is called on the proxy; inside, this.saveAndNotify() is called on the underlying target instance, not the proxy. The target class doesn't know about @Transactional โ€” only the proxy does. So the advice doesn't fire.

This same pattern breaks: @Transactional, @Cacheable, @Async, @PreAuthorize, @Scheduled (indirectly), your custom aspects. Any annotation that relies on AOP proxies.

Fixes โ€‹

  1. Split the class โ€” move saveAndNotify into a separate bean and inject it.
  2. Inject self (ugly but works):
    java
    @Service
    public class OrderService {
        @Autowired @Lazy OrderService self;  // the proxy
        public void placeOrder(Order o) {
            self.saveAndNotify(o);  // goes through the proxy
        }
    }
  3. AopContext.currentProxy() โ€” requires @EnableAspectJAutoProxy(exposeProxy = true).
  4. AspectJ compile-/load-time weaving โ€” avoids the problem entirely because weaving happens on bytecode, not via proxies. Heavyweight.

Other proxy limitations โ€‹

  • private methods are not proxied โ€” the proxy subclass can't see them (CGLIB) or they're not on the interface (JDK).
  • final methods โ€” CGLIB can't override; silently not advised.
  • static methods โ€” not proxied at all.
  • Package-private โ€” usually not advised; depends on proxy strategy.
  • Calls from a constructor or @PostConstruct โ€” the proxy may not be fully installed yet (lifecycle step 10).

Interview Q&A โ€‹

  • How does Spring AOP work? Proxy-based: each bean matching an aspect's pointcut gets wrapped by a JDK or CGLIB proxy during BeanPostProcessor.postProcessAfterInitialization. Method calls on the proxy invoke the advice chain around the target method.
  • Why doesn't @Transactional work when called from the same class? Self-invocation bypasses the proxy โ€” the internal call goes directly on this, not via the proxy, so advice doesn't fire.
  • JDK proxy vs CGLIB? JDK requires an interface, generates an interface-based proxy. CGLIB subclasses the target, works without interfaces but can't override final. Boot defaults to CGLIB.
  • When would you use AspectJ over Spring AOP? When you need method-level granularity beyond Spring-managed beans (including private and final), or when self-invocation is unavoidable. Also for field access pointcuts (Spring AOP doesn't support them).

Pitfalls โ€‹

  • Self-invocation โ€” the #1 interview gotcha, causes silently missing transactions/cache/security.
  • final methods โ€” CGLIB silently skips them with no warning. Use @Transactional on non-final methods only.
  • Package-private methods โ€” also silently skipped depending on strategy; stick to public.
  • Proxy chain order โ€” if both @Transactional and @Cacheable apply to the same method, which runs first? Depends on aspect order (@Order); transactions usually wrap cache.

9. Transactions (@Transactional Deep Dive) โ€‹

Concept โ€‹

@Transactional is "just" an AOP aspect around PlatformTransactionManager. When the advice kicks in, it:

  1. Acquires a TransactionStatus from the transaction manager.
  2. Invokes the target method.
  3. On normal return โ†’ commits.
  4. On RuntimeException or Error โ†’ rolls back.
  5. On checked exception โ†’ commits (unless rollbackFor specifies otherwise).

Canonical usage โ€‹

java
@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orders;
    private final InventoryService inventory;

    @Transactional
    public Order placeOrder(NewOrder req) {
        Order o = orders.save(new Order(req));
        inventory.reserve(o);  // if this throws RuntimeException, everything rolls back
        return o;
    }
}

Propagation โ€‹

PropagationIf no txIf tx exists
REQUIRED (default)Create newJoin existing
REQUIRES_NEWCreate newSuspend existing, create new
NESTEDCreate newCreate savepoint within existing
SUPPORTSRun without txJoin existing
NOT_SUPPORTEDRun without txSuspend existing, run without tx
MANDATORYThrowJoin existing
NEVERRun without txThrow

When to use each:

  • REQUIRED โ€” almost always. Default for a reason.
  • REQUIRES_NEW โ€” audit logging / outbox writes that must persist even if outer tx rolls back.
  • NESTED โ€” part of a larger tx that needs its own rollback point (JDBC savepoints; not supported by all DBs/JPA setups).
  • SUPPORTS โ€” read-only lookup that will enlist if called in a tx, otherwise fine without.
  • NOT_SUPPORTED โ€” long-running, non-transactional work you want to keep outside the current tx (e.g., API calls).
  • MANDATORY โ€” "I refuse to run without an outer tx" โ€” a safety net for internal methods.
  • NEVER โ€” "I refuse to run under a tx" โ€” rare.

Isolation levels โ€‹

LevelDirty readNon-repeatable readPhantom read
READ_UNCOMMITTEDPossiblePossiblePossible
READ_COMMITTEDPreventedPossiblePossible
REPEATABLE_READPreventedPreventedPossible
SERIALIZABLEPreventedPreventedPrevented

Anomalies:

  • Dirty read โ€” read uncommitted data from another tx.
  • Non-repeatable read โ€” same SELECT returns different rows within a tx because another tx updated in between.
  • Phantom read โ€” same range query returns different rows because another tx inserted.

Postgres defaults to READ_COMMITTED; MySQL InnoDB defaults to REPEATABLE_READ; Mongo at the document level has its own semantics.

Rollback rules โ€‹

Default: unchecked exceptions only.

java
@Transactional(rollbackFor = IOException.class)  // roll back on this checked exception
public void importFile() throws IOException { ... }

@Transactional(noRollbackFor = BusinessWarningException.class)  // swallow this one
public void process() { ... }

readOnly = true โ€‹

A hint:

  • Hibernate skips dirty checking โ†’ saves CPU on read-heavy methods.
  • Some DBs (Postgres) enforce it at the transaction level โ†’ write attempts fail.
  • Some connection-pool / replica-routing code can send read-only tx to replicas.

It's not a magic performance switch; it's a correctness + optimization hint.

Programmatic transactions โ€‹

Sometimes annotation-based isn't enough (e.g., fine-grained control, reactive):

java
@Service
@RequiredArgsConstructor
public class BatchService {
    private final TransactionTemplate tt;  // construct from PlatformTransactionManager

    public void runBatch(List<Job> jobs) {
        for (Job j : jobs) {
            tt.execute(status -> {
                processOne(j);
                return null;
            });
        }
    }
}

Reactive equivalent: TransactionalOperator (Spring Framework 5.2+).

Self-invocation (again) โ€‹

Same problem as ยง8. A @Transactional method called from another method in the same class runs with no transaction. Fix by splitting classes, injecting self, or AspectJ.

Distributed transactions โ€‹

XA / 2PC via JTA exists โ€” Spring supports JtaTransactionManager โ€” but:

  • Slow (2PC blocks).
  • Fragile (heuristic outcomes).
  • Many modern systems (MongoDB, Kafka, some JDBC drivers) don't support it well.
  • Cloud-native services often prohibit it.

Modern alternative: the Outbox Pattern.

1. In one DB transaction: write domain change AND insert row into `outbox` table.
2. A separate process reads `outbox`, publishes to Kafka/MQ, marks sent.
3. Consumer is idempotent (handles duplicate publishes from retries).

For a legacy MQ microservice: you don't need XA between the DB and IBM MQ. You write to the DB and the outbox, atomic in one tx. A scheduled job (or a CDC tool like Debezium) drains the outbox to MQ. Consumers dedupe on a message ID.

Interview Q&A โ€‹

  • Walk through all transaction propagation levels. See the table above โ€” hit REQUIRED, REQUIRES_NEW, NESTED, SUPPORTS, NOT_SUPPORTED, MANDATORY, NEVER with one use case each.
  • What exceptions trigger rollback by default? RuntimeException and Error. Checked exceptions do not unless you set rollbackFor.
  • Why doesn't checked exception trigger rollback? Historical JTA convention โ€” checked exceptions represent recoverable conditions. Most people consider this a footgun; it's why Spring-style codebases lean heavily on unchecked exceptions.
  • How do you guarantee exactly-once processing between DB and Kafka/MQ? Outbox pattern: one DB tx writes both the domain data and the outbox row; a relay process publishes; consumers are idempotent. Alternatives: Kafka transactions with transactional outbox read, or XA (not recommended).
  • What is readOnly=true actually doing? Hint to Hibernate to skip dirty checking; hint to DB for optimizations; can trigger read-only enforcement. Not a silver bullet.

Pitfalls โ€‹

  • Checked exceptions silently commit โ€” always set rollbackFor = Exception.class if you have a mixed exception hierarchy and you want uniform rollback.
  • REQUIRES_NEW + exception propagation โ€” inner tx rolls back, outer tx also rolls back if the inner exception propagates. Swallow it in the caller if you actually want independent outcomes.
  • Self-invocation silently skips @Transactional โ€” debug by checking whether the call goes through the proxy.
  • @Transactional on private/final โ€” silently ineffective (AOP limitation). Public, non-final only.
  • readOnly=true is not enforced everywhere โ€” don't rely on it to prevent writes; it's an optimization hint, not a security control.
  • Long-running transactions โ€” hold locks, starve the pool, cause replication lag. Keep transaction boundaries tight.
  • Nested @Transactional on a method that calls a service in a different class โ€” usually that @Transactional wins (joining by REQUIRED), not your outer one. Read propagation carefully.

10. Spring Data (JPA + MongoDB) โ€‹

Concept โ€‹

Spring Data lets you define repositories as interfaces โ€” Spring generates the implementation at runtime (or at build time with Boot 3.5 AOT-optimized repositories). You get CRUD, paging, sorting, and derived queries for free.

Repository hierarchy โ€‹

Repository<T, ID>                          (marker)
    โ†“
CrudRepository<T, ID>                      (save/find/delete)
    โ†“
PagingAndSortingRepository<T, ID>          (+ Pageable, Sort)
    โ†“
ListCrudRepository / ListPagingAndSortingRepository   (Boot 3.0 โ€” List-returning variants)
    โ†“
JpaRepository<T, ID>                       (+ flush, batch, getReferenceById)
MongoRepository<T, ID>                     (+ Mongo specifics)

Pick the narrowest one you actually need.

Derived queries โ€‹

java
public interface HabitRepository extends JpaRepository<Habit, UUID> {

    List<Habit> findByUserIdAndActiveTrue(UUID userId);
    Optional<Habit> findByUserIdAndName(UUID userId, String name);
    long countByUserId(UUID userId);
    Page<Habit> findByUserIdOrderByCreatedAtDesc(UUID userId, Pageable pageable);
    List<Habit> findTop5ByUserIdOrderByCreatedAtDesc(UUID userId);
    boolean existsByUserIdAndName(UUID userId, String name);
    void deleteByUserId(UUID userId);
}

Spring parses method names into queries. Keywords: findBy, And, Or, Between, LessThan, GreaterThan, Like, OrderBy, Top, Distinct, IgnoreCase, Containing.

@Query for more complex cases โ€‹

java
public interface HabitRepository extends JpaRepository<Habit, UUID> {

    @Query("SELECT h FROM Habit h JOIN FETCH h.checkIns WHERE h.user.id = :userId")
    List<Habit> findWithCheckIns(UUID userId);

    @Query(value = "SELECT * FROM habits WHERE user_id = :userId", nativeQuery = true)
    List<Habit> findByUserIdNative(UUID userId);

    @Modifying
    @Query("UPDATE Habit h SET h.active = false WHERE h.user.id = :userId")
    int deactivateAll(UUID userId);
}

@Modifying is required for UPDATE/DELETE (plus Spring will clear the persistence context).

The N+1 problem โ€‹

java
List<Post> posts = repo.findAll();
for (Post p : posts) {
    p.getComments();  // 1 SELECT per post โ€” N+1
}

Fixes:

  • @Query("... JOIN FETCH p.comments") โ€” eager fetch in the query.
  • @EntityGraph(attributePaths = "comments") on the repo method.
  • @BatchSize(25) on the association โ€” batches lazy loads.

Projections โ€‹

java
// Interface-based (closed projection)
public interface HabitSummary {
    UUID getId();
    String getName();
    long getStreakDays();
}

List<HabitSummary> findByUserId(UUID userId);

// Class-based (DTO)
public record HabitDto(UUID id, String name, long streak) {}

@Query("SELECT new com.acme.dto.HabitDto(h.id, h.name, h.streakDays) FROM Habit h WHERE h.user.id = :uid")
List<HabitDto> findDtos(UUID uid);

Pageable + Sort โ€‹

java
Pageable page = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<Habit> result = repo.findByUserId(userId, page);
// result.getContent(), result.getTotalElements(), result.getTotalPages(), result.hasNext()

Beware: Page runs a count query every time. For large offsets, consider Slice (no count) or cursor-based pagination.

Auditing โ€‹

java
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class AuditConfig {
    @Bean
    AuditorAware<String> auditorProvider() {
        return () -> Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
                .map(Authentication::getName);
    }
}

@Entity
@EntityListeners(AuditingEntityListener.class)
class Habit {
    @CreatedDate     Instant createdAt;
    @LastModifiedDate Instant updatedAt;
    @CreatedBy       String createdBy;
    @LastModifiedBy  String updatedBy;
    @Version         Long version;  // optimistic locking
}

JPA entity lifecycle โ€‹

NEW/transient  โ”€ persist() โ”€โ†’  MANAGED  โ”€ remove() โ”€โ†’  REMOVED
                                โ”‚
                          detach/clear/close
                                โ†“
                            DETACHED  โ”€ merge() โ”€โ†’  MANAGED (copy)
  • Transient โ€” just new-ed, not known to the persistence context.
  • Managed โ€” tracked; changes auto-sync at flush.
  • Detached โ€” was managed, but the session closed; changes aren't tracked.
  • Removed โ€” scheduled for deletion at flush.

Lazy loading & LazyInitializationException โ€‹

Accessing a lazy collection outside an open session (transaction) throws. Fix by:

  • Keeping the transaction open (Open-Session-in-View โ€” disabled by default in Boot 3, good).
  • Using a DTO projection instead.
  • JOIN FETCH / entity graphs.

Hibernate caches โ€‹

  • First-level (session-scoped) โ€” always on; de-duplicates within a transaction.
  • Second-level (shared across sessions) โ€” opt-in, configured via hibernate.cache.region.factory_class (EhCache, Infinispan, JCache). Usually not worth the complexity unless you have proven hot read-heavy entities.
  • Query cache โ€” caches query results. Use sparingly; easy to make wrong.

Optimistic vs pessimistic locking โ€‹

java
@Entity
class Account {
    @Version Long version;  // optimistic โ€” Hibernate bumps it on update, fails on stale
}

@Lock(LockModeType.PESSIMISTIC_WRITE)
Account findForUpdate(UUID id);  // SELECT ... FOR UPDATE
  • Optimistic โ€” cheap, best when conflicts are rare; on conflict, retry or fail.
  • Pessimistic โ€” hold a row lock; best when conflicts are common and retry cost is high (financial adjustments).

Spring Data REST โ€‹

Auto-exposes repositories as REST endpoints in HAL format. A productivity app built on spring-boot-starter-data-rest typically looks like:

GET  /habits               โ†’ findAll paged, HAL links
GET  /habits/{id}          โ†’ findById
POST /habits               โ†’ save
PUT  /habits/{id}          โ†’ replace
DELETE /habits/{id}        โ†’ delete

Convenient for CRUD admin backends; restrict exposed methods via @RepositoryRestResource(exported = false) on the repo interface.

MongoDB specifics โ€‹

java
@Document(collection = "habits")
@CompoundIndex(def = "{'userId': 1, 'name': 1}", unique = true)
public class Habit {
    @Id String id;
    @Indexed String userId;
    String name;
    Instant createdAt;
}

public interface HabitRepo extends MongoRepository<Habit, String> {
    List<Habit> findByUserId(String userId);

    @Query("{ 'userId': ?0, 'active': true }")
    List<Habit> findActiveByUser(String userId);
}
  • MongoTemplate for complex ops (aggregations, bulk updates).
  • Transactions require a replica set โ€” not available on a single-node dev Mongo. A dev docker-compose either uses a replica set or skips transactions in dev.
  • Aggregation pipeline via Aggregation.newAggregation(...).

Interview Q&A โ€‹

  • How does Spring Data implement repository interfaces at runtime? RepositoryFactorySupport generates a proxy. Query methods are parsed from method names or read from @Query; each call is executed against an EntityManager (JPA) or MongoOperations (Mongo).
  • How do you diagnose and fix N+1? Turn on SQL logging / Hibernate statistics; use JOIN FETCH or @EntityGraph to fetch associations eagerly, or use DTO projections.
  • getReferenceById vs findById? findById loads the entity eagerly (SELECT now). getReferenceById returns a proxy โ€” no DB hit until you access a property. Useful when you only need the FK for an association.
  • Optimistic vs pessimistic locking โ€” use cases? Optimistic (@Version) for low-conflict, high-throughput scenarios. Pessimistic (@Lock) for high-contention operations where retry is expensive.
  • MongoDB transactions โ€” limitations? Require a replica set; have a time limit (default 60s); cross-shard transactions are expensive. Prefer document-embedding or single-document atomicity where possible.

Pitfalls โ€‹

  • N+1 queries in lazy associations โ€” always the first thing to check on slow endpoints.
  • save() on a detached entity โ€” actually a merge; triggers a SELECT + UPDATE. Check if you meant merge().
  • Flush inside a loop โ€” holds write locks; batch or periodic flush-clear is needed for bulk ops.
  • Leaking Page objects across transaction boundaries** โ€” they contain lazy-loaded content that may fail later.
  • Dev Mongo without replica set blocks transactions โ€” pick one: run a replica set locally, or design without transactions.
  • Ordering on fields without indexes โ€” slow on large collections; check EXPLAIN / Mongo explain() output.

11. Spring Security โ€‹

Concept โ€‹

Spring Security is a chain of servlet filters bolted to the front of your app's filter chain. Every request passes through the chain; filters authenticate, authorize, set security context, handle exceptions.

The filter chain โ€‹

FilterChainProxy holds one or more SecurityFilterChains (e.g., one for /api/**, one for /admin/**). Key filters in order:

  1. DisableEncodeUrlFilter โ€” stops URL rewriting (security: session IDs in URLs).
  2. WebAsyncManagerIntegrationFilter โ€” propagates SecurityContext across @Async.
  3. SecurityContextHolderFilter (Boot 3+; replaces SecurityContextPersistenceFilter).
  4. HeaderWriterFilter โ€” security response headers (CSP, X-Frame-Options, HSTS).
  5. CsrfFilter โ€” CSRF protection (skipped on safe methods).
  6. LogoutFilter โ€” handles /logout.
  7. UsernamePasswordAuthenticationFilter โ€” form login.
  8. DefaultLoginPageGeneratingFilter โ€” generates default login page.
  9. BasicAuthenticationFilter โ€” HTTP Basic.
  10. BearerTokenAuthenticationFilter โ€” OAuth2 Resource Server (if on classpath).
  11. RequestCacheAwareFilter โ€” restores saved request after auth.
  12. SecurityContextHolderAwareRequestFilter โ€” wraps request with security methods.
  13. AnonymousAuthenticationFilter โ€” if not authenticated, sets anonymous principal.
  14. SessionManagementFilter โ€” session fixation, concurrent sessions.
  15. ExceptionTranslationFilter โ€” catches auth exceptions, redirects or returns 401/403.
  16. AuthorizationFilter โ€” Boot 3 / Security 6 replaces the legacy FilterSecurityInterceptor โ€” makes the final authorization decision.

Canonical config (Spring Security 6 / Boot 3) โ€‹

java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Bean
    SecurityFilterChain api(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .csrf(csrf -> csrf.disable())                       // stateless JWT API
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .cors(Customizer.withDefaults())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter()))
            )
            .exceptionHandling(e -> e
                .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
            );
        return http.build();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();  // {bcrypt}... {argon2}... {noop}...
    }
}

Authentication flow โ€‹

HTTP request
    โ†“
Authentication Filter (e.g. UsernamePasswordAuthenticationFilter, BearerTokenAuthenticationFilter)
    โ†“
Builds an Authentication object (not yet authenticated)
    โ†“
AuthenticationManager.authenticate(...)
    โ†“
delegates to one or more AuthenticationProvider(s)
    โ†“
Provider calls UserDetailsService.loadUserByUsername(...)
    โ†“
Compares PasswordEncoder-matched password (form login)
    OR validates JWT signature + claims (resource server)
    โ†“
Returns Authentication (principal + authorities), now authenticated
    โ†“
SecurityContextHolder.getContext().setAuthentication(authn)
    โ†“
Chain continues; AuthorizationFilter enforces rules

Method security โ€‹

java
@Service
public class HabitService {

    @PreAuthorize("hasRole('USER') and #userId == authentication.principal.userId")
    public List<Habit> listForUser(UUID userId) { ... }

    @PostAuthorize("returnObject.userId == authentication.principal.userId")
    public Habit findById(UUID id) { ... }
}
  • @PreAuthorize โ€” checked before method invocation; can reference method args via SpEL.
  • @PostAuthorize โ€” checked after; useful for "did this return MY data?" checks.
  • @PreFilter / @PostFilter โ€” filter collections.
  • Same proxy caveats as ยง8 โ€” method security uses AOP too.

JWT validation (resource server) โ€‹

yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/acme   # auto-discovers JWKS

Boot auto-configures a JwtDecoder that:

  • Fetches the JWK Set from {issuer}/.well-known/jwks.json.
  • Caches keys with rotation.
  • Validates signature, iss, exp, nbf, aud.
  • Produces JwtAuthenticationToken with authorities mapped from scope/scp claims (customizable).

Custom claim-to-authority mapping:

java
JwtAuthenticationConverter jwtAuthConverter() {
    JwtGrantedAuthoritiesConverter authorities = new JwtGrantedAuthoritiesConverter();
    authorities.setAuthoritiesClaimName("roles");
    authorities.setAuthorityPrefix("ROLE_");
    JwtAuthenticationConverter conv = new JwtAuthenticationConverter();
    conv.setJwtGrantedAuthoritiesConverter(authorities);
    return conv;
}

CSRF vs CORS โ€‹

CSRFCORS
What it protectsUsers from forged state-changing requests using their browser cookiesBrowsers from scripts on other origins reading your responses
When neededBrowser session-based auth (cookies)Cross-origin browser requests (SPA on one domain, API on another)
ToolCsrfToken + XSRF-TOKEN cookieCorsConfigurationSource / controller @CrossOrigin
JWT APIsUsually disabled (stateless)Required if the SPA is on another domain

Password encoders โ€‹

Spring Security 5.x+ recommends DelegatingPasswordEncoder โ€” passwords stored with {bcrypt}... / {argon2}... prefixes so you can migrate algorithms without a big-bang re-hash.

BCrypt is the safe default. Argon2 is stronger (memory-hard, GPU-resistant) if you have the deps.

For regulated-environment work โ€‹

  • MFA โ€” required for privileged access; Spring Security + OAuth2 providers (Keycloak, Okta) handle this externally.
  • Audit logging โ€” every auth success/failure, privilege change, data access. Use AuthenticationSuccessEvent / AuthenticationFailureEvent listeners.
  • Crypto โ€” ensure your deployment uses FIPS 140-2 validated modules (Java FIPS provider or OS-level).
  • Session management โ€” limit concurrent sessions, force session invalidation on privilege change.
  • TLS 1.2+ end-to-end; mTLS between services where warranted.

Interview Q&A โ€‹

  • Walk through the Spring Security filter chain. (Hit 5-6 key filters in order: SecurityContextHolderFilter, CsrfFilter, UsernamePasswordAuthenticationFilter / BearerTokenAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter.)
  • How does JWT validation work in Spring Security? JwtDecoder fetches JWKs from the issuer, validates signature and standard claims, produces a JwtAuthenticationToken with authorities mapped from claims.
  • CSRF vs CORS? CSRF prevents forged cross-site state-changing requests using cookies; CORS controls which cross-origin scripts can read your API responses. CSRF is session-cookie-relevant; JWT stateless APIs usually disable it.
  • Method security vs URL-based security? URL-based (authorizeHttpRequests) is coarse (by path); method security (@PreAuthorize) is fine-grained, runs inside the service, supports SpEL referencing method args. Use both โ€” defense in depth.
  • How would you rotate JWT signing keys without downtime? Publish new key in JWKS alongside the old; issuer starts signing with new kid; clients validate against either; old key removed after exp of last-issued token.

Pitfalls โ€‹

  • permitAll() ordering mistakes โ€” .anyRequest().authenticated() before .requestMatchers("/public/**").permitAll() locks out public routes.
  • Forgetting .csrf().disable() on stateless JWT APIs โ€” every POST returns 403 with no clear log reason.
  • ROLE_ prefix auto-injection โ€” hasRole("ADMIN") matches ROLE_ADMIN authority. hasAuthority("ADMIN") matches literal ADMIN. Pick one convention.
  • Method security + self-invocation (same as ยง8) โ€” intra-class call bypasses @PreAuthorize.
  • JWT with alg=none โ€” historical vuln (allowing unsigned tokens); modern libraries reject, but verify your config.
  • Token leakage via logs or error messages โ€” scrub Authorization headers from request loggers.

12. Spring Boot Actuator & Observability โ€‹

Concept โ€‹

Actuator exposes production-ready endpoints for health, metrics, configuration introspection, and diagnostics. Micrometer is the metrics faรงade โ€” you instrument once, export anywhere (Prometheus, Datadog, New Relic, CloudWatch).

Endpoints โ€‹

yaml
management:
  endpoints:
    web:
      exposure:
        include: health, info, metrics, prometheus, loggers
      base-path: /actuator   # default
  endpoint:
    health:
      show-details: when-authorized
      probes:
        enabled: true        # splits into /actuator/health/liveness and /readiness

Common endpoints:

EndpointPurpose
/actuator/healthApp health; composite of HealthIndicators
/actuator/health/livenessK8s liveness probe
/actuator/health/readinessK8s readiness probe
/actuator/infoBuild info, Git commit, custom info
/actuator/metricsAll Micrometer metrics
/actuator/metrics/{name}Drill into one
/actuator/prometheusPrometheus scrape endpoint
/actuator/envEnvironment properties โ€” lock down!
/actuator/configpropsBound @ConfigurationProperties โ€” lock down
/actuator/loggersRuntime log level changes
/actuator/threaddumpJVM thread dump โ€” lock down
/actuator/heapdumpHeap dump file โ€” lock down (HUGE, sensitive)
/actuator/httpexchangesRecent HTTP requests (must enable an exchange repository)
/actuator/mappingsAll request mappings
/actuator/conditionsAuto-config condition report
/actuator/beansBean graph
/actuator/scheduledtasksCron/scheduled jobs
/actuator/startupStartup step timings (requires BufferingApplicationStartup)

Exposure and security: expose only what ops needs publicly (health, info, prometheus). Put others behind auth โ€” ideally on a separate port (management.server.port) so they're not internet-exposed at all.

Health indicators โ€‹

Boot auto-registers indicators for datasources, message brokers, caches, etc. Custom:

java
@Component
public class MqConnectionHealthIndicator implements HealthIndicator {
    private final ConnectionFactory cf;

    @Override
    public Health health() {
        try (Connection c = cf.createConnection()) {
            return Health.up().withDetail("broker", c.getMetaData().getJMSProviderName()).build();
        } catch (JMSException e) {
            return Health.down(e).build();
        }
    }
}

Liveness vs readiness โ€‹

  • Liveness โ€” "is the app dead? restart me if so." Fails = K8s kills pod.
  • Readiness โ€” "should I receive traffic right now?" Fails = K8s removes from service endpoints, pod stays alive.

Boot's ApplicationAvailability interface + AvailabilityChangeEvent lets you signal state changes:

java
AvailabilityChangeEvent.publish(context, LivenessState.BROKEN);
AvailabilityChangeEvent.publish(context, ReadinessState.REFUSING_TRAFFIC);

An ArgoCD + K8s deployment relies on these โ€” a pod that can't reach MQ should flip readiness to REFUSING_TRAFFIC so K8s stops routing requests, but NOT liveness (the pod may recover when MQ comes back).

Micrometer โ€‹

java
@Service
@RequiredArgsConstructor
public class HabitService {
    private final MeterRegistry registry;

    public void checkIn(UUID habitId) {
        Timer.Sample sample = Timer.start(registry);
        try {
            // ... work ...
            registry.counter("habit.checkin", "status", "ok").increment();
        } finally {
            sample.stop(registry.timer("habit.checkin.duration", "service", "habit"));
        }
    }
}

Or declaratively:

java
@Timed(value = "habit.checkin.duration")
@Counted(value = "habit.checkin", extraTags = {"service", "habit"})
public void checkIn(UUID habitId) { ... }

Metric types โ€‹

TypeUse
CounterMonotonically increasing count (requests, errors, messages processed)
GaugeInstantaneous value that can go up or down (queue depth, connection pool usage)
TimerDuration + count (request latency)
DistributionSummaryEvent size distribution (payload sizes)
LongTaskTimerConcurrent long-running tasks (in-flight uploads)

Cardinality warning: tags are multiplicative. user_id as a tag = one metric per user = time-series storage meltdown. Keep tags low-cardinality (status codes, HTTP methods, a handful of service names).

Micrometer Observation API (Boot 3) โ€‹

Replaces Sleuth. One Observation produces both metrics and traces automatically:

java
@Autowired ObservationRegistry registry;

Observation.createNotStarted("order.place", registry)
    .highCardinalityKeyValue("orderId", id)
    .lowCardinalityKeyValue("region", region)
    .observe(() -> orderService.place(req));

Or the annotation:

java
@Observed(name = "order.place", contextualName = "place-order")
public Order place(NewOrder req) { ... }

With io.micrometer:micrometer-tracing-bridge-otel and an OTLP exporter, you get traces shipped to an OTel collector alongside metrics.

OpenTelemetry tie-in โ€‹

A common observability pipeline pairs OpenTelemetry with Elasticsearch:

Spring app (Micrometer Observation + OTel bridge)
    โ†“ OTLP
OpenTelemetry Collector
    โ†“
    โ”œโ”€โ”€ Prometheus (metrics)
    โ”œโ”€โ”€ Jaeger / Tempo (traces)
    โ””โ”€โ”€ Elasticsearch / Loki (logs)

Trace context is propagated via traceparent header (W3C) across service boundaries. MDC injection of trace_id and span_id lets you correlate log lines with trace spans โ€” essential for debugging across 20+ microservices.

Interview Q&A โ€‹

  • Which actuator endpoints do you expose in production? /health, /info, /prometheus. Lock down /env, /configprops, /heapdump, /threaddump to an internal network or mgmt port.
  • Liveness vs readiness probes โ€” what fails if you misconfigure? Liveness failure restarts the pod (can cause thrashing if the failure is transient โ€” e.g., DB hiccup). Readiness failure removes from LB (correct for transient issues).
  • Counter vs gauge vs timer? Counter for monotonic counts, gauge for point-in-time values, timer for measuring duration (and count together).
  • How do you propagate trace context across service boundaries? W3C traceparent / tracestate HTTP headers, auto-populated by Micrometer Tracing + OTel. On the consumer side, extract and continue the trace.
  • What SLIs / SLOs would you set for a REST API? Availability (5xx rate), latency (p50/p95/p99), traffic (RPS), saturation (DB pool usage). RED method: Rate, Errors, Duration.

Pitfalls โ€‹

  • Unauthenticated /env / /heapdump โ€” these leak secrets, give attackers everything they need. Classic exam question.
  • Probe misconfiguration โ€” pointing liveness at an endpoint that depends on the DB โ†’ DB hiccup restarts every pod simultaneously.
  • High-cardinality tags โ€” /api/habits/{id} as a label fills your metric store with millions of series. Tag by route template, not actual path.
  • /actuator/heapdump is GB-sized โ€” expose only via authenticated mgmt port.
  • Exposing /actuator/shutdown without tight auth โ€” anyone can stop your app.

13. Spring Boot Testing โ€‹

Concept โ€‹

The goal is fast, reliable tests at every layer: unit (no Spring), slice (minimal context), integration (real dependencies via Testcontainers).

Test pyramid โ€‹

    /\
   /  \   Few E2E / cross-service
  /____\
 /      \  Some integration / slice tests
/________\  Many unit tests (plain JUnit/Mockito)

@SpringBootTest โ€” full integration โ€‹

java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@AutoConfigureMockMvc
class HabitEndpointIT {

    @Autowired MockMvc mvc;
    @Autowired HabitRepository repo;

    @Test
    void createReturns201() throws Exception {
        mvc.perform(post("/api/habits")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"name":"read 10 pages","frequency":"DAILY"}
                """))
           .andExpect(status().isCreated())
           .andExpect(jsonPath("$.id").exists());
    }
}

webEnvironment modes:

  • MOCK (default) โ€” no real server, use MockMvc.
  • RANDOM_PORT โ€” start an actual server on a random port, use TestRestTemplate/WebTestClient.
  • DEFINED_PORT โ€” use server.port.
  • NONE โ€” no web environment.

Slice tests โ€” faster, focused โ€‹

AnnotationLoads
@WebMvcTest(HabitController.class)Just the MVC tier for one controller + @ControllerAdvice, HttpMessageConverters, filter chain, MockMvc. No services, no repos.
@DataJpaTestJPA repos + EntityManager + DataSource. All tests wrapped in a rollback tx. Uses embedded DB by default (override with @AutoConfigureTestDatabase(replace = NONE) + Testcontainers).
@DataMongoTestMongo repos + MongoTemplate.
@JsonTestJackson configuration + JacksonTester.
@RestClientTestRestTemplate/RestClient + MockRestServiceServer.
@WebFluxTestWebFlux controllers + WebTestClient.
@JdbcTestJdbcTemplate, no JPA.

Slice tests load ~minimum beans โ†’ fast startup, good isolation, force clean layering.

Mocking beans in context โ€‹

Boot 3.4+ prefers @MockitoBean / @MockitoSpyBean (from org.springframework.test.context.bean.override.mockito). Older code uses @MockBean / @SpyBean (deprecated for removal).

java
@WebMvcTest(HabitController.class)
class HabitControllerTest {

    @Autowired MockMvc mvc;
    @MockitoBean HabitService service;   // replaces any HabitService bean with a Mockito mock

    @Test
    void list() throws Exception {
        when(service.list(any())).thenReturn(Page.empty());
        mvc.perform(get("/api/habits")).andExpect(status().isOk());
    }
}

Caveat: every unique combination of @MockitoBean/@MockitoSpyBean busts the context cache. Too many = slow tests.

Testcontainers โ€‹

java
@SpringBootTest
@Testcontainers
class IntegrationIT {

    @Container
    @ServiceConnection       // Boot 3.1+ โ€” auto-wires the container into spring.data.mongodb.*
    static MongoDBContainer mongo = new MongoDBContainer("mongo:7.0");

    @Container
    @ServiceConnection
    static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0"));

    @Test
    void endToEnd() { ... }
}

Before Boot 3.1, you used @DynamicPropertySource:

java
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
    registry.add("spring.data.mongodb.uri", mongo::getReplicaSetUrl);
}

@ServiceConnection is cleaner and supports Postgres, MySQL, Redis, Mongo, Kafka, Neo4j, RabbitMQ, Elasticsearch, Cassandra, and more out of the box.

Boot 3.1 Docker Compose support โ€‹

yaml
# compose.yaml
services:
  mongodb:
    image: mongo:7.0
    ports: ["27017:27017"]

With spring-boot-docker-compose dep, ./gradlew bootRun auto-starts the compose stack and wires connections. Great for local dev.

Testing @Transactional โ€‹

java
@DataJpaTest
class HabitRepoTest {
    // Every @Test runs in a tx that rolls back โ€” no cleanup needed.
    // To test rollback behavior, use @Rollback(false) + manual cleanup, or use a TransactionTemplate.
}

Testing @Async โ€‹

java
@Test
void asyncRunsOffThread() throws Exception {
    CountDownLatch latch = new CountDownLatch(1);
    service.doAsync(latch::countDown);
    assertThat(latch.await(2, SECONDS)).isTrue();
}

Don't test "it ran on a different thread" directly โ€” test the observable behavior.

Testing Kafka consumers โ€‹

Options:

  1. @EmbeddedKafka โ€” in-JVM Kafka broker. Fast startup, shares a port with other tests.
  2. Testcontainers Kafka โ€” real Kafka in a container; mirrors prod more accurately.
  3. Direct consumer test โ€” instantiate your listener class and feed it a ConsumerRecord directly (no broker).

For a legacy MQ microservice, testing a @JmsListener against Testcontainers ActiveMQ is the gold standard โ€” catches serialization, ack timing, redelivery edge cases.

java
@Test
void consumerProcessesMessage(@Autowired KafkaTemplate<String, Order> tpl) throws Exception {
    CountDownLatch latch = new CountDownLatch(1);
    listener.setHook(latch::countDown);

    tpl.send("orders.v1", new Order(...)).get();

    assertThat(latch.await(10, SECONDS)).isTrue();
    verify(orderService).save(any());
}

Testing rules to live by โ€‹

  • Don't mock framework code (Spring itself). Mock your dependencies.
  • Prefer Testcontainers over H2/embedded Mongo โ€” the engine differences bite in prod. Your CLAUDE.md says this explicitly.
  • One assertion cluster per test โ€” clear failure messages.
  • Avoid @DirtiesContext โ€” reloading the context is expensive.
  • Parameterize, don't duplicate โ€” @ParameterizedTest + @CsvSource.

Interview Q&A โ€‹

  • Slice tests vs @SpringBootTest? Slice tests load only the beans for one layer (MVC, JPA, Mongo) โ€” fast, isolated. @SpringBootTest loads everything โ€” slow but the closest to prod.
  • @MockBean context cache effect? Every distinct combination busts the cache. With many tests, this slows the suite to a crawl. Prefer plain Mockito where possible, reserve mocked beans for slice/integration tests.
  • Why Testcontainers over H2? H2's SQL dialect, locking, and JSON support differ from Postgres/MySQL. Tests pass locally, prod queries fail. Testcontainers runs the actual DB engine.
  • How would you test a Kafka consumer end-to-end? Testcontainers Kafka (or @EmbeddedKafka for speed). Publish a record, CountDownLatch in the listener, assert side effects. Don't mock the broker โ€” test the actual consumer wiring, deserialization, and ack path.

Pitfalls โ€‹

  • Context cache busting via @MockBean / @DirtiesContext โ€” silently doubles or triples test suite time.
  • H2 mode tricks (MODE=PostgreSQL) only go so far โ€” JSONB, ON CONFLICT, window-function edge cases differ.
  • Transaction-wrapped tests masking lazy-loading issues that bite in prod (no tx boundary on an API response).
  • Flaky Kafka / async tests โ€” use Awaitility or CountDownLatch, never Thread.sleep.
  • Order-dependent tests โ€” shared state between tests. Every test should be independently runnable.

14. Spring WebFlux & Reactive โ€‹

Concept โ€‹

Spring WebFlux is the reactive, non-blocking counterpart to Spring MVC. It's built on Project Reactor (Mono, Flux) instead of servlet request-response. The claim to fame: handle many concurrent connections with a small, fixed thread pool โ€” because threads are never blocked on I/O.

Mono vs Flux โ€‹

  • Mono<T> โ€” 0 or 1 element, completes or errors.
  • Flux<T> โ€” 0 to N elements, completes or errors.
java
Mono<Habit> findById(UUID id);        // single result
Flux<Habit> findByUserId(UUID userId); // stream

Basic operators โ€‹

java
// map โ€” transform each element synchronously
habits.map(h -> new HabitDto(h.getId(), h.getName()));

// flatMap โ€” transform each element into a Publisher (async)
userIds.flatMap(userRepo::findById);

// filter, take, skip, distinct, concatMap (ordered flatMap)
// zip โ€” combine N publishers into tuples
Mono.zip(userMono, prefsMono, statsMono).map(tuple -> buildProfile(tuple));

// error handling
habits.onErrorResume(ex -> Flux.empty())
      .retry(3)
      .timeout(Duration.ofSeconds(2));

// switchIfEmpty
findByName(name).switchIfEmpty(Mono.error(new NotFoundException()));

Schedulers โ€‹

You run blocking work on boundedElastic; CPU work on parallel:

java
Mono.fromCallable(() -> blockingDbCall())
    .subscribeOn(Schedulers.boundedElastic())
    .publishOn(Schedulers.parallel())
    .map(this::cpuExpensiveTransform)
    .subscribe();
  • immediate โ€” current thread.
  • single โ€” single worker thread (sequential execution).
  • parallel โ€” one worker per CPU (CPU-bound work).
  • boundedElastic โ€” bounded elastic pool, default for blocking I/O (10 ร— CPU capacity).

Backpressure โ€‹

The consumer controls how many items the publisher emits next. request(n) from the subscriber tells the upstream "I can handle N more." For fast producers (e.g. event streams), operators help:

  • onBackpressureBuffer(size) โ€” buffer up to N (watch OOM).
  • onBackpressureDrop(onDrop) โ€” drop + callback.
  • onBackpressureLatest() โ€” keep only the latest.
  • limitRate(n) โ€” throttle from below.
  • sample(Duration) โ€” sample at intervals.

Controllers โ€‹

Annotated style โ€” same as MVC, different return types:

java
@RestController
@RequestMapping("/api/habits")
public class HabitController {

    private final HabitService svc;

    @GetMapping("/{id}")
    public Mono<HabitDto> get(@PathVariable UUID id) {
        return svc.findById(id);
    }

    @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<HabitEvent> stream() {
        return svc.events();  // server-sent events stream
    }
}

Functional style:

java
@Configuration
public class HabitRoutes {
    @Bean
    public RouterFunction<ServerResponse> habitRoutes(HabitHandler h) {
        return route(GET("/api/habits/{id}"), h::get)
              .andRoute(POST("/api/habits"), h::create);
    }
}

WebClient โ€‹

The reactive HTTP client โ€” successor to RestTemplate in reactive code:

java
@Bean
WebClient webClient(WebClient.Builder b) {
    return b.baseUrl("https://upstream.example.com")
            .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
            .build();
}

Mono<User> user = webClient.get()
    .uri("/users/{id}", id)
    .retrieve()
    .bodyToMono(User.class);

R2DBC โ€‹

Reactive JDBC. Works with Spring Data Reactive repositories:

java
public interface HabitRepo extends ReactiveCrudRepository<Habit, UUID> {
    Flux<Habit> findByUserId(UUID userId);
}

When to pick WebFlux โ€” after virtual threads โ€‹

Pre-Java-21, WebFlux was the answer to "I need to handle 10k concurrent connections." In Boot 3.2+ with virtual threads (ยง17), spring.threads.virtual.enabled=true gives you most of the same I/O concurrency story with blocking MVC code. Pick WebFlux for:

  • Streaming / backpressure semantics โ€” SSE, WebSocket broadcasting, reactive Kafka consumers.
  • Fully reactive pipelines โ€” chaining WebClient calls, reactive DB, reactive messaging end-to-end.
  • Existing reactive codebase โ€” don't mix paradigms.

For most high-concurrency REST APIs with blocking dependencies, MVC + virtual threads is simpler and equally fast.

Interview Q&A โ€‹

  • Mono vs Flux? Mono is 0-1 elements, Flux is 0-N. Both are cold (don't execute until subscribed).
  • How does backpressure work? Subscribers request(n) items from upstream; operators like limitRate and onBackpressureBuffer let you shape flow when producers are faster than consumers.
  • WebClient vs RestTemplate vs RestClient? RestTemplate blocking (maintenance-only). WebClient reactive, use in WebFlux. RestClient (Boot 3.2) is blocking but with the WebClient fluent API โ€” new default for MVC.
  • When pick WebFlux vs MVC + virtual threads? WebFlux for streaming, backpressure, fully reactive pipelines. MVC + virtual threads for plain REST APIs with blocking dependencies โ€” simpler, same throughput for the common case.

Pitfalls โ€‹

  • Blocking code on a reactive thread โ€” a Thread.sleep or a JDBC call inside a map starves the event loop. Use .subscribeOn(Schedulers.boundedElastic()).
  • Forgetting to subscribe โ€” reactive is cold. If you log-and-throw-away a Mono, nothing happens.
  • Losing Security/trace context โ€” the SecurityContext propagation works out-of-box with ReactorContextWebFilter, but custom aspects often don't; use ThreadLocalAccessor in Boot 3.
  • Mixing blocking frameworks โ€” Hibernate in a WebFlux controller is a landmine.
  • Debugging stack traces โ€” reactive stacks are cryptic; enable Hooks.onOperatorDebug() in dev or use Reactor Checkpoints.

15. Spring Cloud & Microservices Patterns โ€‹

Concept โ€‹

Spring Cloud is a family of projects that address the operational concerns of distributed systems: config, discovery, routing, resilience, observability.

Spring Cloud Config โ€‹

yaml
# config-server/src/main/resources/application.yml
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/acme/config-repo
          search-paths: '{application}/{profile}'
yaml
# client-app/src/main/resources/application.yml
spring:
  application:
    name: order-service
  config:
    import: configserver:http://config-server:8888

@RefreshScope on a bean + POST to /actuator/refresh reloads config live. Add Spring Cloud Bus (RabbitMQ/Kafka) to broadcast refresh across all replicas.

Service discovery โ€‹

OptionNotes
EurekaSpring's classic; in maintenance
ConsulHashiCorp, richer feature set
Kubernetes nativeIf you're on K8s, use Services; spring-cloud-kubernetes maps ConfigMaps/Secrets too
HashiCorp Nomad, etcdNiche
java
@EnableDiscoveryClient
@SpringBootApplication
public class OrderServiceApp { ... }

@RestController
public class Ctrl {
    @Autowired DiscoveryClient discovery;

    public List<ServiceInstance> instances(String svc) {
        return discovery.getInstances(svc);
    }
}

Client-side load balancing โ€‹

Ribbon is dead (Spring Cloud Netflix is EOL). spring-cloud-loadbalancer is the successor:

java
@LoadBalanced
@Bean
RestClient.Builder loadBalancedRestClient() {
    return RestClient.builder();
}

// Now restClient.get().uri("http://user-service/users/{id}", id) resolves via discovery + LB

API Gateway โ€” Spring Cloud Gateway โ€‹

Built on WebFlux. Declarative route config:

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: orders
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - RewritePath=/api/(?<segment>.*), /${segment}
            - name: CircuitBreaker
              args:
                name: ordersCb
                fallbackUri: forward:/fallback/orders
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20

OpenFeign vs HTTP Interface โ€‹

OpenFeign โ€” long-standing declarative HTTP client:

java
@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {
    @GetMapping("/users/{id}")
    User findById(@PathVariable UUID id);
}

HTTP Interface (@HttpExchange, Boot 3.2+) โ€” lighter, Spring-native, Spring auto-generates the impl with HttpServiceProxyFactory:

java
public interface UserClient {
    @GetExchange("/users/{id}")
    User findById(@PathVariable UUID id);
}

@Bean
UserClient userClient(RestClient.Builder builder) {
    RestClient rc = builder.baseUrl("http://user-service").build();
    return HttpServiceProxyFactory
        .builderFor(RestClientAdapter.create(rc))
        .build()
        .createClient(UserClient.class);
}

HttpExchange has lower overhead, no separate project, works with RestClient or WebClient. New services should prefer it over Feign unless you specifically need a Feign feature.

Resilience4j โ€‹

java
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserClient client;

    @CircuitBreaker(name = "userClient", fallbackMethod = "fallback")
    @Retry(name = "userClient")
    @TimeLimiter(name = "userClient")
    @Bulkhead(name = "userClient")
    public CompletableFuture<User> findById(UUID id) {
        return CompletableFuture.supplyAsync(() -> client.findById(id));
    }

    public CompletableFuture<User> fallback(UUID id, Throwable ex) {
        return CompletableFuture.completedFuture(User.UNKNOWN);
    }
}
yaml
resilience4j:
  circuitbreaker:
    instances:
      userClient:
        sliding-window-size: 20
        minimum-number-of-calls: 10
        failure-rate-threshold: 50   # %
        wait-duration-in-open-state: 10s
        permitted-number-of-calls-in-half-open-state: 5
  retry:
    instances:
      userClient:
        max-attempts: 3
        wait-duration: 500ms
        exponential-backoff-multiplier: 2
        retry-exceptions:
          - java.net.ConnectException

Circuit breaker states:

CLOSED (normal) โ”€ failure rate > threshold โ”€โ†’ OPEN (fail fast)
     โ†‘                                            โ”‚
     โ”‚                                   (after wait-duration)
     โ”‚                                            โ†“
     โ””โ”€โ”€โ”€ success rate ok โ”€โ”€โ”€ HALF_OPEN (probe with N calls)

Distributed tracing โ€” Micrometer Tracing โ€‹

Replaces Spring Cloud Sleuth (EOL). With micrometer-tracing-bridge-otel + OTLP exporter:

  • Every Observation produces a span.
  • traceparent header auto-propagated across RestClient/WebClient/FeignClient.
  • MDC populated with traceId/spanId so log lines correlate.

Spring Cloud Stream โ€‹

Binder abstraction โ€” write a Kafka/Rabbit consumer once, swap brokers via config:

java
@Bean
public Function<KStream<String, Order>, KStream<String, Receipt>> process() {
    return input -> input.filter((k, v) -> v.total() > 0)
                         .mapValues(Receipt::of);
}

Anchor example: Kafka + Avro pipelines are a natural fit for Spring Cloud Stream, but raw spring-kafka gives more control for complex topologies.

Interview Q&A โ€‹

  • How does a circuit breaker work (state machine)? CLOSED (normal, counting failures) โ†’ OPEN (short-circuit, fail fast) when failure rate exceeds threshold โ†’ HALF_OPEN (probe with N calls) after wait duration โ†’ CLOSED on success, OPEN on failure.
  • Client-side vs server-side discovery? Client-side: client queries registry, picks instance (Eureka + LoadBalancer). Server-side: client hits a load balancer (ALB, ingress), which queries registry. Trade-off: client-side is more flexible, server-side is simpler for clients.
  • Spring Cloud Gateway vs Zuul? Gateway is reactive (WebFlux), async, current-gen. Zuul 1 was blocking, Netflix-EOL. Gateway is the answer.
  • When HttpExchange over Feign? HttpExchange is native Spring 6+, uses RestClient/WebClient, lighter and more composable. Feign is fine for existing code; HttpExchange is the default for new code.
  • Retry pitfalls in microservices? Retry storms โ€” every layer retrying cascades. Always combine with circuit breakers, use jitter, and make sure the operation is idempotent.

Pitfalls โ€‹

  • Retry storms without jitter โ€” synchronized retries hammer an already-struggling service.
  • Circuit breaker misconfigured sliding window โ€” too small = flap between closed/open, too large = slow to react.
  • @RefreshScope not applied everywhere โ€” only refresh-scoped beans rebuild on /actuator/refresh. @ConfigurationProperties beans often don't, unless explicitly annotated.
  • Feign default timeouts โ€” 60s is too long for customer-facing. Set per-client sensible values.
  • Config server down at startup โ€” set spring.cloud.config.fail-fast=true to fail-fast (better than starting with stale config).

16. Caching, Scheduling, Async โ€‹

Concept โ€‹

Three aspect-driven features โ€” all rely on AOP, all share the same self-invocation pitfalls as ยง8.

Caching โ€‹

java
@SpringBootApplication
@EnableCaching
public class App { ... }

@Service
public class HabitService {

    @Cacheable(value = "habits", key = "#id")
    public Habit findById(UUID id) { ... }

    @CachePut(value = "habits", key = "#result.id")
    public Habit update(Habit h) { ... }

    @CacheEvict(value = "habits", key = "#id")
    public void delete(UUID id) { ... }

    @Caching(evict = {
        @CacheEvict(value = "habits", key = "#id"),
        @CacheEvict(value = "userHabits", key = "#habit.userId")
    })
    public void deleteWithPrefetch(UUID id, Habit habit) { ... }
}

Key expression: SpEL. #paramName, #result, #root.methodName, #root.args[0].

Conditional caching: @Cacheable(condition = "#id != null", unless = "#result.isEmpty()").

Cache providers โ€‹

ProviderUse
Caffeine (default on classpath)Local, high-performance, L1 cache
RedisDistributed, shared across replicas
HazelcastDistributed, in-JVM with cluster
EhCacheLocal, heap+disk
No-op (default if no provider on classpath)Boot's built-in โ€” no-op unless you've configured one

Cache pitfalls โ€‹

  • Self-invocation (same as ยง8) โ€” calling findById from within the same class skips the cache.
  • Null handling โ€” depends on provider; Caffeine doesn't cache nulls by default, Redis does.
  • Stampede โ€” N concurrent requests for a missing key all hit the DB. Caffeine's AsyncLoadingCache dedupes; Redis can use locks or refreshAfterWrite.
  • Key collisions โ€” if two methods use the same cache name but different key types (e.g., UUID vs String), you can get surprising hits/misses.
  • TTL mismatch โ€” cached DTO goes stale while the underlying row is updated elsewhere. Mitigate with @CacheEvict on the update path.

Scheduling โ€‹

java
@SpringBootApplication
@EnableScheduling
public class App { ... }

@Component
public class HabitDailyReset {

    @Scheduled(cron = "0 0 0 * * ?", zone = "America/Chicago")
    public void resetDaily() { ... }

    @Scheduled(fixedRate = 60_000)  // every 60s, regardless of last run duration
    public void heartbeat() { ... }

    @Scheduled(fixedDelay = 60_000)  // 60s after last completion
    public void pollQueue() { ... }

    @Scheduled(initialDelay = 10_000, fixedDelay = 60_000)
    public void deferredJob() { ... }
}

fixedRate vs fixedDelay:

  • fixedRate โ€” schedule next N ms after start of previous.
  • fixedDelay โ€” schedule next N ms after completion of previous.

For long-running tasks, prefer fixedDelay to avoid overlapping executions (unless you want parallel, and configure the scheduler for it).

Executor configuration: the default TaskScheduler is a single-threaded ThreadPoolTaskScheduler โ€” if two @Scheduled tasks coincide, one blocks the other. Configure:

java
@Bean
TaskScheduler taskScheduler() {
    ThreadPoolTaskScheduler s = new ThreadPoolTaskScheduler();
    s.setPoolSize(5);
    s.setThreadNamePrefix("scheduled-");
    return s;
}

Boot 3.2+ โ€” use virtual threads: spring.threads.virtual.enabled=true makes @Scheduled execute on virtual threads.

Async โ€‹

java
@SpringBootApplication
@EnableAsync
public class App { ... }

@Service
public class NotificationService {

    @Async
    public CompletableFuture<Void> sendEmail(String to, String subject) {
        emailClient.send(to, subject);
        return CompletableFuture.completedFuture(null);
    }
}

Return types supported: void (fire-and-forget โ€” errors swallowed unless you set AsyncUncaughtExceptionHandler), CompletableFuture<T>, Future<T>.

Default executor: before Boot 3.2, SimpleAsyncTaskExecutor spawns unbounded threads โ€” a footgun under load. Always configure:

java
@Bean(name = "taskExecutor")
ThreadPoolTaskExecutor taskExecutor() {
    ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
    ex.setCorePoolSize(10);
    ex.setMaxPoolSize(50);
    ex.setQueueCapacity(200);
    ex.setThreadNamePrefix("async-");
    ex.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    ex.initialize();
    return ex;
}

Boot 3.2+: spring.threads.virtual.enabled=true makes @Async use virtual threads automatically โ€” no custom pool needed for I/O-heavy work.

Anchor example: productivity app scheduling โ€‹

@EnableScheduling goes on the main application class. Background tasks like habit streak recomputation or email reminders fit the @Scheduled model. When the user base grows, this moves out to a dedicated worker service with distributed coordination (ShedLock, Quartz) โ€” but for a single-replica app, @Scheduled is right-sized.

Interview Q&A โ€‹

  • What is the risk of the default @Async executor? SimpleAsyncTaskExecutor (pre-Boot-3.2) spawns unbounded threads, one per call โ€” OOM under load. Always configure a bounded pool or enable virtual threads.
  • fixedRate vs fixedDelay? fixedRate schedules relative to the start of the previous run; fixedDelay relative to completion. fixedDelay is safer for long-running jobs.
  • Cache self-invocation? Same AOP proxy issue โ€” calling a @Cacheable method from another method in the same class bypasses the proxy, so no caching.
  • How do you mitigate cache stampede? Dedupe concurrent computes (Caffeine AsyncLoadingCache), add a mutex, or refreshAfterWrite to compute in background.

Pitfalls โ€‹

  • Unbounded @Async pool on default โ€” see above.
  • @Scheduled on a non-singleton bean โ€” gets registered once per instance, or sometimes not at all; counterintuitive.
  • Caching void methods โ€” makes no sense; Spring won't stop you but you'll get no benefit.
  • Caching inside a transaction โ€” cache key determined before tx commit; stale reads possible.
  • @Async methods in the same class as the caller (self-invocation again) โ€” runs synchronously, silently.

17. Spring Boot 3.x Modern Features โ€‹

Jakarta EE namespace (Boot 3.0) โ€‹

The javax.* โ†’ jakarta.* migration was the headline breaking change of Boot 3.0 / Framework 6:

  • javax.persistence.* โ†’ jakarta.persistence.*
  • javax.validation.* โ†’ jakarta.validation.*
  • javax.servlet.* โ†’ jakarta.servlet.*

Why it happened: Oracle transferred Java EE to the Eclipse Foundation (renamed Jakarta EE). The javax namespace is Oracle's; Eclipse couldn't use it for new APIs. So every enterprise API got renamed.

What broke: any library that hadn't migrated. Most popular libs moved by 2023; legacy deps still may not.

Practical impact: if you're migrating a Boot 2.x app, every import javax.persistence.Entity; becomes import jakarta.persistence.Entity;. IDEs and openrewrite handle most of it.

Java baseline โ€‹

  • Boot 3.0 โ€” Java 17 minimum.
  • Boot 3.2 โ€” adds Java 21 support (virtual threads, sequenced collections).
  • Boot 3.4+ โ€” Java 25 fully supported.

Virtual threads (Project Loom) โ€‹

yaml
spring:
  threads:
    virtual:
      enabled: true

With this one flag:

  • Tomcat uses virtual threads for request threads.
  • @Async executor uses virtual threads.
  • @Scheduled tasks run on virtual threads.
  • Spring RabbitMQ / Kafka listeners โ€” depending on version.

Why it matters: blocking I/O (JDBC, HTTP client calls) no longer ties up a platform thread. You can handle 10k concurrent requests with a handful of carrier threads. Most of the "I need WebFlux" motivation disappears for apps that are I/O-bound but not stream-heavy.

Pinning caveats:

  • Long synchronized blocks โ†’ pinning (virtual thread sticks to its carrier). java.util.concurrent.locks.ReentrantLock doesn't pin.
  • JNI/native calls โ†’ pinning.
  • Thread.currentThread().getId() is billion-range; don't use thread IDs as cache keys.

GraalVM native image / AOT โ€‹

bash
mvn -Pnative native:compile

You get a standalone executable:

  • Startup: ~200ms (vs ~11s on JVM for PetClinic).
  • Memory: ~80MB (vs ~210MB JVM).
  • Build time: minutes (AOT compilation is expensive).

How it works: Spring generates AOT code at build time (reachability metadata, no-reflection bean factories, precomputed configuration). The native-image compiler does closed-world analysis and produces machine code.

Caveats:

  • Reflection/dynamic class loading needs reachability-metadata.json hints.
  • Some libraries don't work natively (check the GraalVM reachability repo).
  • Build is slow โ€” keep JVM for dev, native for CI/CD of release artifacts.

AOT-optimized repositories (Boot 3.5+) โ€‹

Spring Data now generates repository implementations at build time instead of runtime proxies. Benefits: faster startup, smaller footprint, better IDE introspection (you can debug into the generated class).

Problem Details (RFC 7807) โ€‹

yaml
spring:
  mvc:
    problemdetails:
      enabled: true

Built-in Spring exceptions now return structured application/problem+json:

json
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "Required parameter 'id' is missing",
  "instance": "/api/habits"
}

Wrap your own domain errors by extending ErrorResponseException or returning ProblemDetail from @ExceptionHandler.

RestClient (Boot 3.2) โ€‹

Synchronous, fluent HTTP client โ€” RestTemplate replacement with WebClient-style API:

java
@Bean
RestClient restClient(RestClient.Builder b) {
    return b.baseUrl("https://api.example.com").build();
}

User u = restClient.get()
    .uri("/users/{id}", id)
    .retrieve()
    .body(User.class);

Vs RestTemplate: same blocking semantics, better API, easier testing (MockRestServiceServer supported). Vs WebClient: blocking; use when you don't want to return a Mono/Flux.

JdbcClient (Boot 3.2) โ€‹

Fluent JDBC:

java
List<Habit> habits = jdbcClient.sql("SELECT * FROM habits WHERE user_id = :uid")
    .param("uid", userId)
    .query(Habit.class)
    .list();

Replaces the verbose JdbcTemplate + NamedParameterJdbcTemplate combo.

HTTP Interface clients (@HttpExchange) โ€‹

Covered in ยง15. @GetExchange, @PostExchange, etc. โ€” Spring generates the impl with HttpServiceProxyFactory. Works over RestClient or WebClient.

CRaC (Coordinated Restore at Checkpoint) โ€‹

JVM snapshot-and-restore: take a checkpoint of a warmed-up process, restart from checkpoint in milliseconds. Boot 3.2+ supports Lifecycle hooks so beans can do before-checkpoint cleanup (close DB connections, etc.) and after-restore resumption.

Useful for serverless / FaaS cold starts; niche otherwise.

Micrometer Observation (replaces Sleuth) โ€‹

Unified metrics + traces via one API (ObservationRegistry, @Observed). See ยง12. Sleuth was EOL with Boot 3.

Structured logging (Boot 3.4) โ€‹

yaml
logging:
  structured:
    format:
      console: ecs   # or logstash, gelf

JSON log output out of the box, with standard fields (trace ID, span ID, MDC). Ships straight to Elasticsearch/ECS without a logstash pipeline.

spring-boot-docker-compose module โ€‹

With spring-boot-docker-compose on the classpath, ./mvnw spring-boot:run starts services listed in compose.yaml, wires their connections via spring.datasource.url etc., and shuts them down on exit. Local-dev ergonomics for free.

Interview Q&A โ€‹

  • What changed in Boot 3? Java 17 baseline, Jakarta namespace migration, Spring Framework 6, native image primary-supported, Problem Details, RestClient, new observation API.
  • Virtual threads + Spring โ€” what actually changes? One flag enables virtual threads for Tomcat, @Async, @Scheduled, JMS/Kafka listeners. Blocking code scales like reactive without rewriting as reactive.
  • Native vs JVM โ€” when pick native? Native for cold-start-sensitive (FaaS, on-demand scaling), memory-constrained (sidecars), fast-startup batch jobs. JVM for dev cycle speed, complex dynamic classloading, warm-throughput-critical workloads.
  • RestClient vs RestTemplate vs WebClient? RestTemplate is legacy. RestClient is the new blocking default (same API shape as WebClient). WebClient for reactive.
  • Jakarta migration โ€” what broke? Everything that imported javax.* under the old Java EE packages. OpenRewrite recipes or IDE refactors handle most of it; watch for libraries that lag.

Pitfalls โ€‹

  • Virtual thread pinning โ€” long synchronized blocks / JNI pin the virtual to the carrier, defeating the benefit. Migrate synchronized to ReentrantLock in hot paths.
  • Native reflection โ€” untested reflection-heavy libs silently fail at runtime. Test the native build in CI, not just post-release.
  • Jakarta migration with mixed-version deps โ€” one library still on javax.servlet will NoClassDefFoundError in surprising places. openrewrite helps but check classpath.
  • Stale RestTemplate code โ€” kept "for now" tends to silently degrade as Spring focuses maintenance on RestClient/WebClient.

18. Messaging Integration (JMS, Kafka, AMQP) โ€‹

Concept โ€‹

Spring provides listener-container abstractions for the common broker types. Same pattern everywhere: a *Template to send, a listener container to receive, configurable error handling and concurrency.

Spring JMS (IBM MQ / ActiveMQ / Artemis) โ€‹

This is the primary territory for a legacy MQ microservice (10k+ tx/day).

java
@Configuration
@EnableJms
public class JmsConfig {

    @Bean
    public DefaultJmsListenerContainerFactory jmsFactory(ConnectionFactory cf, JmsErrorHandler eh) {
        DefaultJmsListenerContainerFactory f = new DefaultJmsListenerContainerFactory();
        f.setConnectionFactory(cf);
        f.setConcurrency("3-10");             // min-max consumers
        f.setSessionTransacted(true);         // participates in transaction
        f.setErrorHandler(eh);
        return f;
    }

    @Bean
    public JmsTemplate jmsTemplate(ConnectionFactory cf) {
        JmsTemplate t = new JmsTemplate(cf);
        t.setDeliveryPersistent(true);
        t.setTimeToLive(60_000);
        return t;
    }
}

@Component
public class OrderListener {

    @JmsListener(destination = "ORDERS.IN", containerFactory = "jmsFactory")
    public void onMessage(OrderDto order, @Header(JmsHeaders.MESSAGE_ID) String mid) {
        orderService.process(order);
    }
}

MessageConverter: MappingJackson2MessageConverter for JSON-over-JMS; custom for Avro/XML/binary.

Backout (dead-letter) queue: IBM MQ convention โ€” after N redelivery failures, the message is moved to a backout queue (often named ORDERS.IN.BOQ). Configure via queue properties + MQ-specific listener settings.

Transactional listener: with setSessionTransacted(true), the listener's ack happens inside a JMS transaction; throwing redelivers.

Spring Kafka โ€‹

java
@Configuration
@EnableKafka
public class KafkaConfig {

    @Bean
    public ConsumerFactory<String, Order> consumerFactory() {
        Map<String, Object> props = Map.of(
            BOOTSTRAP_SERVERS_CONFIG, "kafka:9092",
            GROUP_ID_CONFIG, "order-service",
            KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class,
            VALUE_DESERIALIZER_CLASS_CONFIG, KafkaAvroDeserializer.class,
            SCHEMA_REGISTRY_URL_CONFIG, "http://schema-registry:8081",
            ENABLE_AUTO_COMMIT_CONFIG, false,
            ISOLATION_LEVEL_CONFIG, "read_committed"
        );
        return new DefaultKafkaConsumerFactory<>(props);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, Order> kafkaFactory(
            ConsumerFactory<String, Order> cf, DefaultErrorHandler eh) {
        var f = new ConcurrentKafkaListenerContainerFactory<String, Order>();
        f.setConsumerFactory(cf);
        f.setConcurrency(3);
        f.getContainerProperties().setAckMode(AckMode.MANUAL_IMMEDIATE);
        f.setCommonErrorHandler(eh);
        return f;
    }

    @Bean
    public DefaultErrorHandler errorHandler(KafkaTemplate<Object, Object> tpl) {
        DeadLetterPublishingRecoverer dlt = new DeadLetterPublishingRecoverer(tpl);
        ExponentialBackOff backoff = new ExponentialBackOff(1000L, 2.0);
        backoff.setMaxInterval(60_000L);
        backoff.setMaxElapsedTime(300_000L);
        return new DefaultErrorHandler(dlt, backoff);
    }
}

@Component
public class OrderListener {

    @KafkaListener(topics = "orders.v1", containerFactory = "kafkaFactory")
    public void onMessage(Order o, Acknowledgment ack) {
        orderService.process(o);
        ack.acknowledge();
    }
}

Key patterns:

  • Manual ack (AckMode.MANUAL_IMMEDIATE) โ€” ack after successful processing, not before. Commit-after-process.
  • DLT via DeadLetterPublishingRecoverer โ€” Spring Kafka publishes to {original-topic}.DLT after retries exhausted.
  • RetryTopicConfiguration โ€” non-blocking retries via successive retry topics (better than in-listener blocking retries).
  • Avro + Schema Registry โ€” cross-team schema standardization work lives here. Consumers deserialize using registered schemas; producers write using subject-name strategy.

Spring AMQP (RabbitMQ) โ€‹

java
@Configuration
@EnableRabbit
public class RabbitConfig {
    @Bean Queue orders() { return new Queue("orders", true); }
    @Bean DirectExchange ordersEx() { return new DirectExchange("orders.ex"); }
    @Bean Binding binding(Queue orders, DirectExchange ordersEx) {
        return BindingBuilder.bind(orders).to(ordersEx).with("orders");
    }
}

@Component
public class OrderListener {
    @RabbitListener(queues = "orders")
    public void onMessage(Order o) { ... }
}

Topology declared in Java; RabbitAdmin auto-declares at startup.

Transactional messaging โ€‹

Chained transactions across DB + messaging:

java
@Bean
public ChainedTransactionManager chainedTx(JpaTransactionManager jpa, JmsTransactionManager jms) {
    return new ChainedTransactionManager(jpa, jms);
}

ChainedTransactionManager is deprecated but still works; it commits in order (JPA first, JMS second), so a JMS failure after JPA commit is still a data inconsistency. The outbox pattern (ยง9) is the recommended replacement.

Kafka transactions โ€” exactly-once semantics (EOS):

java
@Bean
public KafkaTransactionManager<Object, Object> ktm(ProducerFactory<Object, Object> pf) {
    return new KafkaTransactionManager<>(pf);
}

Producer factory must set transactional.id. Combined with read_committed consumer isolation, you get atomic read-process-write across Kafka topics.

Anchor example: legacy MQ bridge architecture โ€‹

Mainframe โ†’ IBM MQ (ORDERS.IN)
              โ†“
        Spring Boot listener (@JmsListener)
              โ†“
        Validation + enrichment
              โ†“
        DB transaction write (JPA) + outbox insert
              โ†“ (scheduled relay)
        Kafka publish (Avro + Schema Registry)
              โ†“
        Downstream microservices consume
              โ†“
        AWS ActiveMQ (bridge for legacy consumers)

Concerns along the way:

  • Message dedup โ€” JMSMessageID or domain-level idempotency key, stored in a processed_messages table.
  • Redelivery โ€” exponential backoff in listener error handler; backout queue after N retries.
  • Monitoring โ€” consumer lag, DLT depth, processing latency per topic.

Interview Q&A โ€‹

  • @JmsListener container concurrency โ€” how do you tune it? setConcurrency("3-10") sets min-max consumers per destination. Tune based on processing time and peak throughput; watch for ordering guarantees if needed.
  • How do you set up a DLT in Spring Kafka? DeadLetterPublishingRecoverer + DefaultErrorHandler with a backoff. Failed messages after retries are published to {topic}.DLT.
  • Kafka transactions โ€” how do they give exactly-once? Producer writes + consumer offset commit in one transaction. isolation.level=read_committed on downstream consumers skips in-flight/aborted txns.
  • How do you handle a poison pill? ErrorHandlingDeserializer catches deserialization errors, passes a wrapped DeserializationException to the error handler, which routes to DLT instead of looping.
  • When Kafka vs IBM MQ? MQ: transactional, legacy integration, per-message semantics, broker-centric, mainframe interop. Kafka: high-throughput streaming, log retention, stream processing, multi-consumer-group fan-out.

Pitfalls โ€‹

  • Swallowed exceptions in listeners โ€” @JmsListener method doesn't rethrow โ†’ listener container thinks it succeeded, commits the ack. Always rethrow or route through an error handler.
  • Auto-commit masking failures โ€” Kafka enable.auto.commit=true commits offsets on schedule regardless of processing. Use manual ack.
  • Missing idempotency โ€” a retried message reprocesses and double-writes. Always dedupe on consumer side (idempotency key, DB unique constraint).
  • JMS SESSION_TRANSACTED vs CLIENT_ACKNOWLEDGE โ€” people confuse them; SESSION_TRANSACTED integrates with Spring's transaction, CLIENT_ACKNOWLEDGE is JMS-native.
  • Avro schema incompatibility at runtime โ€” producer publishes a breaking schema change; consumers crash. Enforce BACKWARD compatibility in the schema registry (the classic cross-team standardization lesson).

19. Performance, Startup & Tuning โ€‹

Startup time โ€‹

  • spring.main.lazy-initialization=true โ€” beans instantiated on first use. Trades startup time for first-request latency. Hides config errors until runtime.
  • @ComponentScan(basePackages = "com.acme.service") โ€” narrow scan > default (the application class's package and all subpackages).
  • Exclude unused auto-configs: @SpringBootApplication(exclude = {...}). Run with --debug to see what's applied.
  • Class Data Sharing (CDS) โ€” Java 25: java -XX:ArchiveClassesAtExit=app.jsa -jar app.jar on startup; subsequent starts with -XX:SharedArchiveFile=app.jsa are faster.
  • BufferingApplicationStartup: captures startup step timings at /actuator/startup; find the slow beans.
java
public static void main(String[] args) {
    SpringApplication app = new SpringApplication(MyApp.class);
    app.setApplicationStartup(new BufferingApplicationStartup(1024));
    app.run(args);
}
  • Native image (ยง17) for sub-second startup when that's a dominant concern.

Connection pool tuning (HikariCP) โ€‹

Defaults:

SettingDefaultRecommendation
maximum-pool-size10CPU count ร— (2..4) for most OLTP; measure under load
minimum-idlesame as maxLeave equal unless you know you want elasticity
connection-timeout30s1-5s for user-facing; fail-fast is better than fail-slow
idle-timeout10minCheck DB idle-kill policy
max-lifetime30minMust be less than DB-side wait_timeout
leak-detection-threshold0 (off)30s-60s in dev/staging; helps find unclosed tx

Rule of thumb: pool size = (core_count ร— 2) + effective_spindle_count, capped by DB's max_connections / replica_count. Oversized pools cause DB contention, not speed.

Embedded server tuning โ€‹

Tomcat defaults are generous (200 threads). For resource-constrained containers:

yaml
server:
  tomcat:
    threads:
      max: 100
      min-spare: 10
    accept-count: 100
    connection-timeout: 5s
    max-keep-alive-requests: 100

With virtual threads (ยง17), server.tomcat.threads.max is effectively unbounded โ€” the request thread doesn't block on I/O, so concurrency is limited by memory and downstream capacity, not threads.

JVM tuning โ€‹

  • Heap: container-aware -XX:MaxRAMPercentage=75 (Boot's Dockerfile templates do this). Don't hardcode -Xmx in cloud/container deployments.
  • GC: G1 default (Java 17+), good all-rounder. ZGC (low-latency, large heaps) when p99 matters. Parallel GC for throughput batch jobs.
  • Flight Recorder: -XX:StartFlightRecording=filename=rec.jfr,duration=60s in production for deep diagnosis.

Docker multi-stage build (productivity app example) โ€‹

dockerfile
# Stage 1: frontend build
FROM node:22-slim AS frontend
COPY frontend /app
WORKDIR /app
RUN npm ci && npm run build

# Stage 2: Maven build
FROM maven:3.9-eclipse-temurin-25 AS backend
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline  # cache deps
COPY src src
COPY --from=frontend /app/dist src/main/resources/static
RUN mvn clean package -DskipTests

# Stage 3: runtime
FROM eclipse-temurin:25-jre
COPY --from=backend /app/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

Small runtime image (no build tools), good layer cache ordering (deps first, code last).

Profile / diagnose slow prod endpoints โ€‹

  1. /actuator/metrics/http.server.requests โ€” p50/p95/p99 latency by URI template.
  2. /actuator/metrics/hikaricp.connections.pending โ€” pool saturation.
  3. /actuator/threaddump โ€” what's everyone blocked on?
  4. /actuator/heapdump (to disk, download, analyze with Eclipse MAT or JVisualVM).
  5. /actuator/prometheus โ€” correlate with external metrics.
  6. Distributed traces (ยง12) โ€” where in the pipeline is the time going?

Interview Q&A โ€‹

  • How do you speed up Spring Boot startup? Lazy init (with caveats), narrower component scan, exclude unused auto-configs, measure with /actuator/startup. For dramatic improvement: native image.
  • How do you size HikariCP? Start at 10; raise based on measured wait time + DB connection headroom. Cap max-lifetime below DB wait_timeout. Enable leak detection in non-prod.
  • ZGC vs G1 in a Spring app? G1 default โ€” good all-rounder. ZGC when sub-10ms pause times matter (user-facing APIs with strict p99 SLOs, large heaps > 32GB). Parallel for pure throughput batch.
  • Why is your prod endpoint slow when local is fast? Network latency to DB/downstream, different data volumes, cold caches, pool saturation. Start with /actuator/metrics/http.server.requests + thread dump.

Pitfalls โ€‹

  • Lazy init hiding startup errors โ€” a misconfigured bean fails on first request in prod, not in dev. Use lazy init carefully.
  • Oversized thread pools in K8s โ€” container has 1 CPU limit, app spawns 200 Tomcat threads, all context-switching. Size pools to container shape.
  • max-lifetime > DB wait_timeout โ€” DB kills the connection, Hikari hands it out, user sees "connection closed."
  • Hardcoded -Xmx in container deployments โ€” JVM doesn't respect the container memory limit unless container-aware flags are on.

20. Common Scenario-Based Interview Questions โ€‹

1. "Your endpoint is fast locally but slow in prod. Debug it." โ€‹

Answer shape:

  1. Narrow the symptom: is it all endpoints or one? Check /actuator/metrics/http.server.requests for p50/p95/p99 by URI template.
  2. Where is the time going? Distributed trace (OpenTelemetry) โ€” DB? Downstream API? Internal CPU?
  3. Resource pressure: thread dump (blocked threads?), heap dump (memory pressure?), CPU profile (hot methods?).
  4. Downstream: DB slow query log, connection pool saturation (hikaricp.connections.pending), network latency to external deps.
  5. Fix: add an index, cache a hot read, bulkhead the downstream, scale out.

2. "How would you add a new data source to an existing Spring Boot app?" โ€‹

java
@Configuration
public class SecondaryDataSourceConfig {

    @Bean
    @ConfigurationProperties("app.secondary.datasource")
    public DataSourceProperties secondaryProps() { return new DataSourceProperties(); }

    @Bean
    public DataSource secondaryDataSource() {
        return secondaryProps().initializeDataSourceBuilder().build();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean secondaryEmf(
            EntityManagerFactoryBuilder b, DataSource secondaryDataSource) {
        return b.dataSource(secondaryDataSource)
                .packages("com.acme.secondary.domain")
                .persistenceUnit("secondary")
                .build();
    }

    @Bean
    public PlatformTransactionManager secondaryTxManager(
            @Qualifier("secondaryEmf") EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}

Use @Transactional("secondaryTxManager") to pick which tx manager. The primary DS gets @Primary.

3. "Two beans depend on each other โ€” how do you resolve?" โ€‹

  1. Redesign โ€” is one of them really two responsibilities? Extract the shared bit into a third bean.
  2. @Lazy on one injection โ€” breaks the cycle with a lazy proxy.
  3. Setter injection on one side โ€” works with older circular-dep allowance.

Don't opt back into spring.main.allow-circular-references=true โ€” it papers over a design problem.

4. "Run a task when the app starts โ€” what are your options?" โ€‹

OptionFires atNotes
Constructor / @PostConstructBean initBean's deps are wired but app not necessarily ready. No @Transactional.
InitializingBean.afterPropertiesSetSame as @PostConstructPrefer @PostConstruct.
ApplicationRunner / CommandLineRunnerAfter context refresh, before main() returnsRuns once. Good for DB warmup, cache preload.
@EventListener(ApplicationReadyEvent.class)After ApplicationRunnersLatest hook; app is fully ready. Best for "start consumers," "publish to service discovery."
SmartLifecycleControlled orderingWhen you need precise start/stop phases.

For a legacy MQ microservice: message consumer startup goes in ApplicationReadyEvent or SmartLifecycle โ€” you don't want the listener pulling before Actuator health is green.

5. "Design a multi-tenant Spring Boot app." โ€‹

Tenant resolution (how you figure out who's calling):

  • Subdomain (tenant1.app.com)
  • Header (X-Tenant-Id)
  • JWT claim
  • Path prefix (/t/{tenantId}/...)

Resolve in a filter โ†’ store in TenantContext (ThreadLocal) โ†’ clear on response (remember virtual threads!).

Isolation strategies:

StrategyProsCons
Database-per-tenantStrong isolation, easy backup/restore per tenantOperational cost grows linearly
Schema-per-tenant (Postgres)Strong isolation, shared DB instanceConnection pool per schema; migration complexity
Row-level (tenant_id column + WHERE everywhere)Cheap, simpleEasy to leak data with a missing filter; harder to tune per tenant

Spring's AbstractRoutingDataSource handles DB-per-tenant transparently:

java
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
    @Override protected Object determineCurrentLookupKey() { return TenantContext.get(); }
}

For row-level, use Hibernate filters (@Filter) or a @PrePersist / query interceptor โ€” but the safest is a repo layer that always takes a tenant ID arg.

6. "Introduce a new microservice with zero downtime." โ€‹

  1. Contract test first โ€” Pact between new service and its callers, validated in CI.
  2. Deploy dark โ€” service up, no traffic routed.
  3. Smoke tests in prod env โ€” synthetic requests against the new service.
  4. Shadow traffic โ€” mirror real traffic, compare responses (API gateway can do this).
  5. Canary โ€” ArgoCD Rollouts / Istio / Linkerd: 1% โ†’ 5% โ†’ 25% โ†’ 100% with health checks.
  6. Rollback plan โ€” ArgoCD app rollback or revert Git commit.

7. "Your @Transactional method doesn't roll back. Debug it." โ€‹

Checklist:

  1. Checked exception? Default rollback is unchecked only โ†’ add rollbackFor = Exception.class.
  2. Self-invocation? Method called from another method in the same class โ†’ proxy bypassed, no tx at all.
  3. Private method? Proxies don't advise private methods.
  4. final method? CGLIB can't override final methods.
  5. No @EnableTransactionManagement (Boot enables it automatically, but a manual @Configuration might not).
  6. Wrong transaction manager in a multi-DS setup โ€” transaction opened against the wrong DS.
  7. readOnly=true? Some DB drivers enforce it; write attempts fail with a cryptic error.
  8. Exception caught internally without rethrow โ€” silent no-rollback.

8. "Design API versioning across 10+ microservices." โ€‹

Version in the URL (/v1/habits, /v2/habits) โ€” pragmatic, easy to route at the gateway, discoverable. Downside: feels un-REST-y.

Version in a header (Accept: application/vnd.acme.v2+json) โ€” purer REST, harder to test in a browser.

OpenAPI contracts committed to a shared repo; every service publishes its spec to a central catalog.

Deprecation policy: Deprecation + Sunset headers, 6-month cutover window, dashboards tracking v1 usage so you know when you can actually remove it.

Breaking vs non-breaking: add fields = backward-compatible, remove fields = breaking. Stay backward-compatible within a major version.


21. Quick-Reference Cheat Sheet โ€‹

Stereotype annotations โ€‹

AnnotationUse
@ComponentGeneric bean
@ServiceBusiness logic
@RepositoryPersistence + DB exception translation
@ControllerMVC controller (views)
@RestController@Controller + @ResponseBody
@ConfigurationJava-based bean definitions

Bean scopes โ€‹

ScopeLifetime
singletonContainer (default)
prototypePer getBean
requestPer HTTP request
sessionPer HTTP session
applicationPer ServletContext
websocketPer WebSocket session

Lifecycle (singleton bean) โ€‹

instantiate โ†’ populate โ†’ *Aware โ†’ BPP.before โ†’ @PostConstruct โ†’ afterPropertiesSet โ†’ init-method โ†’ BPP.after (AOP proxy here) โ†’ READY โ†’ @PreDestroy โ†’ destroy() โ†’ destroy-method

Propagation + isolation โ€‹

Propagation:  REQUIRED (default) | REQUIRES_NEW | NESTED | SUPPORTS | NOT_SUPPORTED | MANDATORY | NEVER
Isolation:    READ_UNCOMMITTED โ†’ READ_COMMITTED โ†’ REPEATABLE_READ โ†’ SERIALIZABLE
Rollback:     default = RuntimeException/Error only  โ†’  rollbackFor = Exception.class for checked

Spring Security filter chain (key filters, in order) โ€‹

SecurityContextHolderFilter โ†’ HeaderWriterFilter โ†’ CsrfFilter โ†’ LogoutFilter โ†’
UsernamePasswordAuthenticationFilter / BearerTokenAuthenticationFilter โ†’
RequestCacheAwareFilter โ†’ SecurityContextHolderAwareRequestFilter โ†’
AnonymousAuthenticationFilter โ†’ SessionManagementFilter โ†’
ExceptionTranslationFilter โ†’ AuthorizationFilter

Actuator endpoints โ€” default safe exposure โ€‹

Expose: health, info, prometheus Lock down (internal/authenticated only): env, configprops, heapdump, threaddump, loggers, shutdown

Boot 3 migration checklist โ€‹

  • [ ] javax.* โ†’ jakarta.* across all imports
  • [ ] Java 17+ baseline
  • [ ] Spring Security 6 lambda DSL
  • [ ] spring.factories โ†’ AutoConfiguration.imports
  • [ ] @MockBean โ†’ @MockitoBean (Boot 3.4+)
  • [ ] Sleuth โ†’ Micrometer Tracing
  • [ ] Consider enabling Problem Details, virtual threads, RestClient

HTTP client decision โ€‹

ClientUse when
RestClient (Boot 3.2+)Blocking, new code
WebClientReactive / WebFlux
RestTemplateLegacy, avoid in new code
@HttpExchangeDeclarative, lightweight
@FeignClientExisting Feign codebase; richer features

@Conditional* quick list โ€‹

@ConditionalOnClass              @ConditionalOnMissingClass
@ConditionalOnBean               @ConditionalOnMissingBean
@ConditionalOnProperty           @ConditionalOnResource
@ConditionalOnWebApplication     @ConditionalOnNotWebApplication
@ConditionalOnExpression         @ConditionalOnJava
@ConditionalOnCloudPlatform

22. Further Reading & Official Docs โ€‹

Primary references โ€‹

For messaging / observability / resilience specifics โ€‹

Opinionated deep-dive blogs โ€‹

  • Baeldung (baeldung.com) โ€” wide breadth, search for any topic.
  • Spring blog (spring.io/blog) โ€” release notes, new feature deep dives.
  • InfoQ Spring coverage โ€” milestone releases, architectural analysis.
  • Vlad Mihalcea (vladmihalcea.com) โ€” the authoritative JPA/Hibernate performance resource.
  • Marco Behler (marcobehler.com) โ€” Spring internals, well-written.

Books โ€‹

  • Pro Spring (Harrop, Machacek, et al.) โ€” still a solid reference.
  • Cloud Native Spring in Action (Vitale) โ€” Boot 3 / Cloud era.
  • Java Persistence with Hibernate (Bauer, King, Gregory) โ€” the canonical JPA deep-dive.

End of guide. If you can walk through every section here with concrete examples โ€” ideally tied to something you actually shipped โ€” you're ready for a senior Spring interview at any level.

Last updated: