Appearance
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:
- Concept โ what the thing is, why it exists, how it fits into the broader Spring model.
- Canonical code โ minimal idiomatic snippet you could reproduce on a whiteboard.
- Interview Q&A โ questions you'll actually be asked, with compressed answer hints.
- 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 โ
- Spring Core & the IoC Container
- Dependency Injection
- Bean Lifecycle & Bean Post-Processors
- Spring Boot Fundamentals & Auto-Configuration
- Externalized Configuration & Profiles
- Spring MVC & REST
- Exception Handling & Validation
- Spring AOP & the Proxy Model
- Transactions (@Transactional Deep Dive)
- Spring Data (JPA + MongoDB)
- Spring Security
- Spring Boot Actuator & Observability
- Spring Boot Testing
- Spring WebFlux & Reactive
- Spring Cloud & Microservices Patterns
- Caching, Scheduling, Async
- Spring Boot 3.x Modern Features
- Messaging Integration (JMS, Kafka, AMQP)
- Performance, Startup & Tuning
- Common Scenario-Based Interview Questions
- Quick-Reference Cheat Sheet
- 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.
BeanFactory | ApplicationContext | |
|---|---|---|
| Bean instantiation | Lazy (on getBean()) | Eager (singletons at startup) |
| AOP auto-proxying | No (manual) | Yes |
| Event publication | No | Yes (ApplicationEvent, @EventListener) |
i18n (MessageSource) | No | Yes |
Environment abstraction | No | Yes |
| Default in Spring Boot | No | Yes |
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. AddsPersistenceExceptionTranslationPostProcessorwhich translates vendor-specificSQLExceptions into Spring'sDataAccessExceptionhierarchy.@Controllerโ Spring MVC controller (produces views).@RestControllerโ@Controller+@ResponseBody(produces JSON/XML bodies).
Bean scopes โ
| Scope | When created | Use case |
|---|---|---|
singleton (default) | Once per container | Stateless services, DAOs |
prototype | Every getBean() call | Stateful helpers you want fresh each call |
request | Per HTTP request | Request-scoped state |
session | Per HTTP session | User session cache |
application | Per ServletContext | Static config per app |
websocket | Per WebSocket session | WS 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.
BeanFactoryvsApplicationContext?ApplicationContextis 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.@Repositoryadds 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)orObjectProvider<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
@Beanmethods inside a@Configurationclass 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:
- Immutability โ
finalfields, set once, thread-safe by construction. - Required deps are obvious โ can't instantiate without them; compiler helps you.
- Testable without Spring โ
new HabitService(mockRepo, Clock.systemUTC())in a plain JUnit test. - Circular deps fail loudly at startup rather than being silently resolved and biting you later.
- No reflection gymnastics in tests (
ReflectionTestUtils.setFieldis 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 โ
| Annotation | Origin | Quirk |
|---|---|---|
@Autowired | Spring | By-type injection |
@Inject | JSR-330 (jakarta.inject) | Same as @Autowired but portable |
@Resource | JSR-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 โ AConstructor 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:
- Redesign โ extract shared logic into a third bean. Circular deps are almost always a hint that two classes have overlapping responsibilities.
@Lazyon one side โ breaks the cycle by proxying that dep.- 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 typeHandler, orders by@Order/Ordered/@Priority, injects. - How do you handle two beans of the same type?
@Primaryfor 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 = silentnull. PreferOptional<Bean>orObjectProvider<Bean>.@Resourcename semantics โ bites people who assume it's just@Autowiredwith 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-methodBeanPostProcessor vs BeanFactoryPostProcessor โ
BeanFactoryPostProcessor | BeanPostProcessor | |
|---|---|---|
| When it fires | Before any bean is instantiated | On every bean, around initialization |
| Operates on | BeanDefinition metadata | Actual bean instances |
| Typical use | Resolve ${placeholders}, mutate config | AOP proxy wrapping, annotation processing |
| Example | PropertySourcesPlaceholderConfigurer | AnnotationAwareAspectJAutoProxyCreator, 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 anyMessageListenerwith a central registry). - Difference between
BeanPostProcessorandBeanFactoryPostProcessor? 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 โ@PostConstructruns in step 7, proxy wrapping happens in step 10. If you call a@Transactionalmethod from@PostConstructonthis, no transaction.
Pitfalls โ
- Heavy work in constructors โ a slow DB ping in a constructor delays startup; put it in
@PostConstructorApplicationReadyEvent. @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
@PostConstructtime โ all deps are set by step 4, so this is usually fine, but if a dep is another bean's@PostConstructside effect, you need@DependsOnor event listening. - Shutdown ordering โ long-running
@PreDestroycan block JVM shutdown beyondmanagement.endpoint.shutdown.enabledgrace period. UseSmartLifecycle.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 โ
- Spring Boot looks in every jar on the classpath for
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Boot 2.7+; older versions usedMETA-INF/spring.factorieskeyed byEnableAutoConfiguration). - 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 - Each auto-config class is a
@Configurationannotated with a cocktail of@ConditionalOn*โ it only applies when its conditions pass.
Conditional annotations โ
| Annotation | Applies when |
|---|---|
@ConditionalOnClass | Class is on the classpath |
@ConditionalOnMissingClass | Class is NOT on the classpath |
@ConditionalOnBean | A bean of given type/name exists |
@ConditionalOnMissingBean | The user hasn't already defined one |
@ConditionalOnProperty | A property is present (and optionally equals a value) |
@ConditionalOnResource | A classpath/filesystem resource exists |
@ConditionalOnWebApplication | Running as a web app |
@ConditionalOnNotWebApplication | NOT a web app |
@ConditionalOnExpression | SpEL expression is true |
@ConditionalOnJava | Running on a specific Java version |
@ConditionalOnCloudPlatform | Cloud 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 depsUsers 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/CommandLineRunnerbeans after refresh. - Installs
FailureAnalyzers โ the pretty "Port 8080 is in use" error comes fromPortInUseFailureAnalyzer.
Debugging auto-config โ
- Run with
--debugโ dumps theConditionEvaluationReport(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.importsfiles; each auto-config class is gated by@ConditionalOn*; your own@Beandefinitions win via@ConditionalOnMissingBean; discovery is handled by@EnableAutoConfiguration. - What is
@SpringBootApplicationcomposed of?@SpringBootConfiguration(specialization of@Configuration) +@EnableAutoConfiguration+@ComponentScan. - How do you disable a specific auto-config?
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)or the propertyspring.autoconfigure.exclude. - Walk through writing a custom starter. Two modules:
-autoconfigurewith@AutoConfigurationclasses and the imports file;-starteras a thin aggregator pulling in runtime deps.
Pitfalls โ
@ConditionalOnMissingBeanis a double-edged sword โ a misplaced user bean silently disables Boot defaults.@ComponentScandefault 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 toAutoConfiguration.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) โ
DevToolsglobal settings (~/.config/spring-boot/)@TestPropertySource/ test-specific- Command-line args (
--server.port=9090) SPRING_APPLICATION_JSONenv var (inline JSON)ServletConfiginit paramsServletContextinit params- JNDI attributes (
java:comp/env) - Java system properties (
-Dkey=val) - OS environment variables
- Profile-specific
application-{profile}.ymloutside the jar - Profile-specific
application-{profile}.ymlinside the jar application.ymloutside the jarapplication.ymlinside the jar@PropertySourceon@Configuration- 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 case | Tool |
|---|---|
| Single scalar, no reuse | @Value |
| Grouped config, type-safe, validated | @ConfigurationProperties |
| Dynamic/computed key lookup | Environment |
@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_attemptsThis 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.)
@Valuevs@ConfigurationProperties?@Valuefor one-off scalars,@ConfigurationPropertiesfor grouped, validated, type-safe config โ always pick@ConfigurationPropertiesfor 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 โ
@Valuewithout a default + missing property =IllegalArgumentExceptionat startup. Always default or wrap inOptionalsemantics.- Profile-specific files don't inherit across profiles โ
application-dev.ymldoesn't "extend"application.ymlin any inheritance sense; both are merged into theEnvironment, with profile-specific winning key-by-key. - Boot 2.4+ changed profile-specific loading โ multi-document YAML with
spring.config.activate.on-profilereplaces the older convention. Don't mix styles. @ConfigurationPropertieson 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 responseIf 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 โ
Filter | HandlerInterceptor | @ControllerAdvice | |
|---|---|---|---|
| Runs at | Servlet container level, before DispatcherServlet | Inside DispatcherServlet, around handler | After the handler throws or a cross-cutting response customization |
| Sees | Raw HttpServletRequest/Response | HandlerMethod, model, view | Method args, thrown exceptions |
| Typical use | Auth (Spring Security), CORS, request logging | Metric timing, auditing, tenant resolution | Global 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.
@RestControllervs@Controller?@RestController=@Controller+@ResponseBodyon every method โ for JSON/XML APIs.@Controllerreturns view names resolved byViewResolver.- How does Spring pick a JSON converter? By matching the request/response media types against registered
HttpMessageConverters, refined by controllerconsumes/producesattributes. - 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
@RequestParamand@PathVariableโ the former is from query string, the latter from the URI template. - CORS pre-flight โ
OPTIONSrequests need to be permitted without auth; Spring Security'scors()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: trueNow 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) { ... }@Validtriggers 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?
@RestControllerAdvicewith@ExceptionHandlermethods mapping exception types toResponseEntityorProblemDetail. - What is
ProblemDetail? RFC 7807 implementation in Spring 6 / Boot 3 โ standard machine-readable error format for HTTP APIs. @Validvs@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
MethodArgumentNotValidExceptionandConstraintViolationException? The first comes from@Validon@RequestBody/controller args; the second from@Validatedon service methods. Both need handlers.
Pitfalls โ
@ControllerAdviceordering โ if you have multiple advice beans, they can compete. Use@Orderor be very narrow inbasePackages/assignableTypes.- Service-layer validation not firing โ you need
@Validatedon the class, and the class must be a Spring-managed bean (proxies again, ยง8). - Re-throwing loses the root cause โ always pass
causethrough 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
@Aspectclass). - 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 โ
| Expression | Matches |
|---|---|
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 proxy | CGLIB | |
|---|---|---|
| How | Generates a proxy class that implements the target's interfaces | Generates a subclass of the target |
| Requires | Target must implement an interface | Target class must not be final |
| Instance type | The interface only | The concrete class |
| Default in Boot 2.0+ | Only if target has an interface and you opted out | Default โ 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 โ
- Split the class โ move
saveAndNotifyinto a separate bean and inject it. - 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 } } AopContext.currentProxy()โ requires@EnableAspectJAutoProxy(exposeProxy = true).- AspectJ compile-/load-time weaving โ avoids the problem entirely because weaving happens on bytecode, not via proxies. Heavyweight.
Other proxy limitations โ
privatemethods are not proxied โ the proxy subclass can't see them (CGLIB) or they're not on the interface (JDK).finalmethods โ CGLIB can't override; silently not advised.staticmethods โ 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
@Transactionalwork when called from the same class? Self-invocation bypasses the proxy โ the internal call goes directly onthis, 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
privateandfinal), 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.
finalmethods โ CGLIB silently skips them with no warning. Use@Transactionalon non-final methods only.- Package-private methods โ also silently skipped depending on strategy; stick to
public. - Proxy chain order โ if both
@Transactionaland@Cacheableapply 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:
- Acquires a
TransactionStatusfrom the transaction manager. - Invokes the target method.
- On normal return โ commits.
- On
RuntimeExceptionorErrorโ rolls back. - On checked exception โ commits (unless
rollbackForspecifies 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 โ
| Propagation | If no tx | If tx exists |
|---|---|---|
REQUIRED (default) | Create new | Join existing |
REQUIRES_NEW | Create new | Suspend existing, create new |
NESTED | Create new | Create savepoint within existing |
SUPPORTS | Run without tx | Join existing |
NOT_SUPPORTED | Run without tx | Suspend existing, run without tx |
MANDATORY | Throw | Join existing |
NEVER | Run without tx | Throw |
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 โ
| Level | Dirty read | Non-repeatable read | Phantom read |
|---|---|---|---|
READ_UNCOMMITTED | Possible | Possible | Possible |
READ_COMMITTED | Prevented | Possible | Possible |
REPEATABLE_READ | Prevented | Prevented | Possible |
SERIALIZABLE | Prevented | Prevented | Prevented |
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?
RuntimeExceptionandError. Checked exceptions do not unless you setrollbackFor. - 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=trueactually 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.classif 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. @Transactionalonprivate/finalโ silently ineffective (AOP limitation). Public, non-final only.readOnly=trueis 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
@Transactionalon a method that calls a service in a different class โ usually that@Transactionalwins (joining byREQUIRED), 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} โ deleteConvenient 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);
}MongoTemplatefor complex ops (aggregations, bulk updates).- Transactions require a replica set โ not available on a single-node dev Mongo. A dev
docker-composeeither 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?
RepositoryFactorySupportgenerates a proxy. Query methods are parsed from method names or read from@Query; each call is executed against anEntityManager(JPA) orMongoOperations(Mongo). - How do you diagnose and fix N+1? Turn on SQL logging / Hibernate statistics; use
JOIN FETCHor@EntityGraphto fetch associations eagerly, or use DTO projections. getReferenceByIdvsfindById?findByIdloads the entity eagerly (SELECT now).getReferenceByIdreturns 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 meantmerge().- Flush inside a loop โ holds write locks; batch or periodic flush-clear is needed for bulk ops.
- Leaking
Pageobjects 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/ Mongoexplain()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:
DisableEncodeUrlFilterโ stops URL rewriting (security: session IDs in URLs).WebAsyncManagerIntegrationFilterโ propagates SecurityContext across@Async.SecurityContextHolderFilter(Boot 3+; replacesSecurityContextPersistenceFilter).HeaderWriterFilterโ security response headers (CSP, X-Frame-Options, HSTS).CsrfFilterโ CSRF protection (skipped on safe methods).LogoutFilterโ handles/logout.UsernamePasswordAuthenticationFilterโ form login.DefaultLoginPageGeneratingFilterโ generates default login page.BasicAuthenticationFilterโ HTTP Basic.BearerTokenAuthenticationFilterโ OAuth2 Resource Server (if on classpath).RequestCacheAwareFilterโ restores saved request after auth.SecurityContextHolderAwareRequestFilterโ wraps request with security methods.AnonymousAuthenticationFilterโ if not authenticated, sets anonymous principal.SessionManagementFilterโ session fixation, concurrent sessions.ExceptionTranslationFilterโ catches auth exceptions, redirects or returns 401/403.AuthorizationFilterโ Boot 3 / Security 6 replaces the legacyFilterSecurityInterceptorโ 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 rulesMethod 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 JWKSBoot 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
JwtAuthenticationTokenwith authorities mapped fromscope/scpclaims (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 โ
| CSRF | CORS | |
|---|---|---|
| What it protects | Users from forged state-changing requests using their browser cookies | Browsers from scripts on other origins reading your responses |
| When needed | Browser session-based auth (cookies) | Cross-origin browser requests (SPA on one domain, API on another) |
| Tool | CsrfToken + XSRF-TOKEN cookie | CorsConfigurationSource / controller @CrossOrigin |
| JWT APIs | Usually 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/AuthenticationFailureEventlisteners. - 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?
JwtDecoderfetches JWKs from the issuer, validates signature and standard claims, produces aJwtAuthenticationTokenwith 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 afterexpof 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")matchesROLE_ADMINauthority.hasAuthority("ADMIN")matches literalADMIN. 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
Authorizationheaders 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 /readinessCommon endpoints:
| Endpoint | Purpose |
|---|---|
/actuator/health | App health; composite of HealthIndicators |
/actuator/health/liveness | K8s liveness probe |
/actuator/health/readiness | K8s readiness probe |
/actuator/info | Build info, Git commit, custom info |
/actuator/metrics | All Micrometer metrics |
/actuator/metrics/{name} | Drill into one |
/actuator/prometheus | Prometheus scrape endpoint |
/actuator/env | Environment properties โ lock down! |
/actuator/configprops | Bound @ConfigurationProperties โ lock down |
/actuator/loggers | Runtime log level changes |
/actuator/threaddump | JVM thread dump โ lock down |
/actuator/heapdump | Heap dump file โ lock down (HUGE, sensitive) |
/actuator/httpexchanges | Recent HTTP requests (must enable an exchange repository) |
/actuator/mappings | All request mappings |
/actuator/conditions | Auto-config condition report |
/actuator/beans | Bean graph |
/actuator/scheduledtasks | Cron/scheduled jobs |
/actuator/startup | Startup 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 โ
| Type | Use |
|---|---|
| Counter | Monotonically increasing count (requests, errors, messages processed) |
| Gauge | Instantaneous value that can go up or down (queue depth, connection pool usage) |
| Timer | Duration + count (request latency) |
| DistributionSummary | Event size distribution (payload sizes) |
| LongTaskTimer | Concurrent 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,/threaddumpto 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/tracestateHTTP 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/heapdumpis GB-sized โ expose only via authenticated mgmt port.- Exposing
/actuator/shutdownwithout 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, useMockMvc.RANDOM_PORTโ start an actual server on a random port, useTestRestTemplate/WebTestClient.DEFINED_PORTโ useserver.port.NONEโ no web environment.
Slice tests โ faster, focused โ
| Annotation | Loads |
|---|---|
@WebMvcTest(HabitController.class) | Just the MVC tier for one controller + @ControllerAdvice, HttpMessageConverters, filter chain, MockMvc. No services, no repos. |
@DataJpaTest | JPA repos + EntityManager + DataSource. All tests wrapped in a rollback tx. Uses embedded DB by default (override with @AutoConfigureTestDatabase(replace = NONE) + Testcontainers). |
@DataMongoTest | Mongo repos + MongoTemplate. |
@JsonTest | Jackson configuration + JacksonTester. |
@RestClientTest | RestTemplate/RestClient + MockRestServiceServer. |
@WebFluxTest | WebFlux controllers + WebTestClient. |
@JdbcTest | JdbcTemplate, 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:
@EmbeddedKafkaโ in-JVM Kafka broker. Fast startup, shares a port with other tests.- Testcontainers Kafka โ real Kafka in a container; mirrors prod more accurately.
- Direct consumer test โ instantiate your listener class and feed it a
ConsumerRecorddirectly (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.@SpringBootTestloads everything โ slow but the closest to prod. @MockBeancontext 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
@EmbeddedKafkafor speed). Publish a record,CountDownLatchin 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
AwaitilityorCountDownLatch, neverThread.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); // streamBasic 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 likelimitRateandonBackpressureBufferlet you shape flow when producers are faster than consumers. WebClientvsRestTemplatevsRestClient?RestTemplateblocking (maintenance-only).WebClientreactive, 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.sleepor a JDBC call inside amapstarves 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; useThreadLocalAccessorin 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 useReactor 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 โ
| Option | Notes |
|---|---|
| Eureka | Spring's classic; in maintenance |
| Consul | HashiCorp, richer feature set |
| Kubernetes native | If you're on K8s, use Services; spring-cloud-kubernetes maps ConfigMaps/Secrets too |
| HashiCorp Nomad, etcd | Niche |
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 + LBAPI 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: 20OpenFeign 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.ConnectExceptionCircuit 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
Observationproduces a span. traceparentheader auto-propagated acrossRestClient/WebClient/FeignClient.MDCpopulated withtraceId/spanIdso 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.
@RefreshScopenot applied everywhere โ only refresh-scoped beans rebuild on/actuator/refresh.@ConfigurationPropertiesbeans 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=trueto 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 โ
| Provider | Use |
|---|---|
| Caffeine (default on classpath) | Local, high-performance, L1 cache |
| Redis | Distributed, shared across replicas |
| Hazelcast | Distributed, in-JVM with cluster |
| EhCache | Local, 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
findByIdfrom 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
AsyncLoadingCachededupes; Redis can use locks orrefreshAfterWrite. - Key collisions โ if two methods use the same cache name but different key types (e.g.,
UUIDvsString), you can get surprising hits/misses. - TTL mismatch โ cached DTO goes stale while the underlying row is updated elsewhere. Mitigate with
@CacheEvicton 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
@Asyncexecutor?SimpleAsyncTaskExecutor(pre-Boot-3.2) spawns unbounded threads, one per call โ OOM under load. Always configure a bounded pool or enable virtual threads. fixedRatevsfixedDelay?fixedRateschedules relative to the start of the previous run;fixedDelayrelative to completion.fixedDelayis safer for long-running jobs.- Cache self-invocation? Same AOP proxy issue โ calling a
@Cacheablemethod 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, orrefreshAfterWriteto compute in background.
Pitfalls โ
- Unbounded
@Asyncpool on default โ see above. @Scheduledon a non-singleton bean โ gets registered once per instance, or sometimes not at all; counterintuitive.- Caching
voidmethods โ 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.
@Asyncmethods 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: trueWith this one flag:
- Tomcat uses virtual threads for request threads.
@Asyncexecutor uses virtual threads.@Scheduledtasks 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
synchronizedblocks โ pinning (virtual thread sticks to its carrier).java.util.concurrent.locks.ReentrantLockdoesn'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:compileYou 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.jsonhints. - 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: trueBuilt-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, gelfJSON 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
synchronizedblocks / JNI pin the virtual to the carrier, defeating the benefit. MigratesynchronizedtoReentrantLockin 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.servletwillNoClassDefFoundErrorin surprising places.openrewritehelps but check classpath. - Stale
RestTemplatecode โ 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}.DLTafter 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 โ
JMSMessageIDor domain-level idempotency key, stored in aprocessed_messagestable. - Redelivery โ exponential backoff in listener error handler; backout queue after N retries.
- Monitoring โ consumer lag, DLT depth, processing latency per topic.
Interview Q&A โ
@JmsListenercontainer 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+DefaultErrorHandlerwith 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_committedon downstream consumers skips in-flight/aborted txns. - How do you handle a poison pill?
ErrorHandlingDeserializercatches deserialization errors, passes a wrappedDeserializationExceptionto 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 โ
@JmsListenermethod 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=truecommits 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_TRANSACTEDvsCLIENT_ACKNOWLEDGEโ people confuse them;SESSION_TRANSACTEDintegrates with Spring's transaction,CLIENT_ACKNOWLEDGEis JMS-native. - Avro schema incompatibility at runtime โ producer publishes a breaking schema change; consumers crash. Enforce
BACKWARDcompatibility 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--debugto see what's applied. - Class Data Sharing (CDS) โ Java 25:
java -XX:ArchiveClassesAtExit=app.jsa -jar app.jaron startup; subsequent starts with-XX:SharedArchiveFile=app.jsaare 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:
| Setting | Default | Recommendation |
|---|---|---|
maximum-pool-size | 10 | CPU count ร (2..4) for most OLTP; measure under load |
minimum-idle | same as max | Leave equal unless you know you want elasticity |
connection-timeout | 30s | 1-5s for user-facing; fail-fast is better than fail-slow |
idle-timeout | 10min | Check DB idle-kill policy |
max-lifetime | 30min | Must be less than DB-side wait_timeout |
leak-detection-threshold | 0 (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: 100With 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-Xmxin 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=60sin 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 โ
/actuator/metrics/http.server.requestsโ p50/p95/p99 latency by URI template./actuator/metrics/hikaricp.connections.pendingโ pool saturation./actuator/threaddumpโ what's everyone blocked on?/actuator/heapdump(to disk, download, analyze with Eclipse MAT or JVisualVM)./actuator/prometheusโ correlate with external metrics.- 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> DBwait_timeoutโ DB kills the connection, Hikari hands it out, user sees "connection closed."- Hardcoded
-Xmxin 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:
- Narrow the symptom: is it all endpoints or one? Check
/actuator/metrics/http.server.requestsfor p50/p95/p99 by URI template. - Where is the time going? Distributed trace (OpenTelemetry) โ DB? Downstream API? Internal CPU?
- Resource pressure: thread dump (blocked threads?), heap dump (memory pressure?), CPU profile (hot methods?).
- Downstream: DB slow query log, connection pool saturation (
hikaricp.connections.pending), network latency to external deps. - 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?" โ
- Redesign โ is one of them really two responsibilities? Extract the shared bit into a third bean.
@Lazyon one injection โ breaks the cycle with a lazy proxy.- 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?" โ
| Option | Fires at | Notes |
|---|---|---|
Constructor / @PostConstruct | Bean init | Bean's deps are wired but app not necessarily ready. No @Transactional. |
InitializingBean.afterPropertiesSet | Same as @PostConstruct | Prefer @PostConstruct. |
ApplicationRunner / CommandLineRunner | After context refresh, before main() returns | Runs once. Good for DB warmup, cache preload. |
@EventListener(ApplicationReadyEvent.class) | After ApplicationRunners | Latest hook; app is fully ready. Best for "start consumers," "publish to service discovery." |
SmartLifecycle | Controlled ordering | When 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:
| Strategy | Pros | Cons |
|---|---|---|
| Database-per-tenant | Strong isolation, easy backup/restore per tenant | Operational cost grows linearly |
| Schema-per-tenant (Postgres) | Strong isolation, shared DB instance | Connection pool per schema; migration complexity |
Row-level (tenant_id column + WHERE everywhere) | Cheap, simple | Easy 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." โ
- Contract test first โ Pact between new service and its callers, validated in CI.
- Deploy dark โ service up, no traffic routed.
- Smoke tests in prod env โ synthetic requests against the new service.
- Shadow traffic โ mirror real traffic, compare responses (API gateway can do this).
- Canary โ ArgoCD Rollouts / Istio / Linkerd: 1% โ 5% โ 25% โ 100% with health checks.
- Rollback plan โ ArgoCD
app rollbackor revert Git commit.
7. "Your @Transactional method doesn't roll back. Debug it." โ
Checklist:
- Checked exception? Default rollback is unchecked only โ add
rollbackFor = Exception.class. - Self-invocation? Method called from another method in the same class โ proxy bypassed, no tx at all.
- Private method? Proxies don't advise private methods.
finalmethod? CGLIB can't override final methods.- No
@EnableTransactionManagement(Boot enables it automatically, but a manual@Configurationmight not). - Wrong transaction manager in a multi-DS setup โ transaction opened against the wrong DS.
readOnly=true? Some DB drivers enforce it; write attempts fail with a cryptic error.- 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 โ
| Annotation | Use |
|---|---|
@Component | Generic bean |
@Service | Business logic |
@Repository | Persistence + DB exception translation |
@Controller | MVC controller (views) |
@RestController | @Controller + @ResponseBody |
@Configuration | Java-based bean definitions |
Bean scopes โ
| Scope | Lifetime |
|---|---|
singleton | Container (default) |
prototype | Per getBean |
request | Per HTTP request |
session | Per HTTP session |
application | Per ServletContext |
websocket | Per WebSocket session |
Lifecycle (singleton bean) โ
instantiate โ populate โ *Aware โ BPP.before โ @PostConstruct โ afterPropertiesSet โ init-method โ BPP.after (AOP proxy here) โ READY โ @PreDestroy โ destroy() โ destroy-methodPropagation + 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 checkedSpring Security filter chain (key filters, in order) โ
SecurityContextHolderFilter โ HeaderWriterFilter โ CsrfFilter โ LogoutFilter โ
UsernamePasswordAuthenticationFilter / BearerTokenAuthenticationFilter โ
RequestCacheAwareFilter โ SecurityContextHolderAwareRequestFilter โ
AnonymousAuthenticationFilter โ SessionManagementFilter โ
ExceptionTranslationFilter โ AuthorizationFilterActuator 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 โ
| Client | Use when |
|---|---|
RestClient (Boot 3.2+) | Blocking, new code |
WebClient | Reactive / WebFlux |
RestTemplate | Legacy, avoid in new code |
@HttpExchange | Declarative, lightweight |
@FeignClient | Existing Feign codebase; richer features |
@Conditional* quick list โ
@ConditionalOnClass @ConditionalOnMissingClass
@ConditionalOnBean @ConditionalOnMissingBean
@ConditionalOnProperty @ConditionalOnResource
@ConditionalOnWebApplication @ConditionalOnNotWebApplication
@ConditionalOnExpression @ConditionalOnJava
@ConditionalOnCloudPlatform22. Further Reading & Official Docs โ
Primary references โ
- Spring Framework reference โ https://docs.spring.io/spring-framework/reference/
- Spring Boot reference โ https://docs.spring.io/spring-boot/docs/current/reference/html/
- Spring Security reference โ https://docs.spring.io/spring-security/reference/
- Spring Data JPA reference โ https://docs.spring.io/spring-data/jpa/reference/
- Spring Data MongoDB reference โ https://docs.spring.io/spring-data/mongodb/reference/
- Spring Cloud โ https://spring.io/projects/spring-cloud
- Project Reactor reference โ https://projectreactor.io/docs/core/release/reference/
For messaging / observability / resilience specifics โ
- Spring Kafka โ https://docs.spring.io/spring-kafka/reference/
- Spring JMS โ https://docs.spring.io/spring-framework/reference/integration/jms.html
- Confluent Avro + Schema Registry โ https://docs.confluent.io/platform/current/schema-registry/
- Testcontainers + Spring Boot โ https://java.testcontainers.org/modules/spring-boot/
- Micrometer + OTel โ https://micrometer.io/docs/tracing
- Resilience4j โ https://resilience4j.readme.io/
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.