โ† Back to Design & Development
Interview Prep

Spring Boot Interview Questions

IoC ยท Beans ยท MVC ยท Data JPA ยท Security ยท Microservices โ€” the way you'd teach a friend over chai

01

Inversion of Control & Dependency Injection โ€” The Idea Spring is Built On

Every Spring interview opens here. The trick is not to recite "IoC means inversion of control" โ€” interviewers have heard that line a thousand times. They want to see if you understand what got inverted and why it matters in real code.

Aman writes a checkout service that needs a payment gateway. He types new RazorpayClient() inside CheckoutService. A week later, his team switches to Stripe. Now every place that new'd a Razorpay client has to change. Multiply that across 80 services and you'll never sleep again. This is the pain Spring's IoC was designed to remove.
What's the difference between IoC and DI? Why does Spring use them?

The "inversion" in plain English

Without IoC, your class controls its dependencies โ€” it new's them up directly. With IoC, that control is inverted: a container creates the dependencies and hands them to your class. Your class just declares "I need a PaymentGateway" โ€” it doesn't pick which implementation, doesn't construct it, doesn't manage its lifecycle.

Dependency Injection is one specific technique to achieve IoC โ€” handing dependencies in via the constructor, a setter, or a field, rather than letting the class build them itself.

Without DI โ€” tight coupling
class CheckoutService {
    private RazorpayClient rp = new RazorpayClient();   // hard-coded

    public void pay(Order o) { rp.charge(o.amount); }
}
With DI โ€” the dependency is handed in
@Service
class CheckoutService {
    private final PaymentGateway gw;

    public CheckoutService(PaymentGateway gw) {   // Spring will inject
        this.gw = gw;
    }

    public void pay(Order o) { gw.charge(o.amount); }
}
Without IoC, you're a chef who buys their own groceries before every meal โ€” slow, tightly coupled to specific stores. With IoC, you're a chef in a hotel kitchen โ€” a stocking team places the right ingredients on your station before service. You declare what you need; the kitchen system arranges it. You can swap suppliers (Razorpay โ†’ Stripe) without changing the chef's recipes.

Why this matters beyond aesthetics

  • Testability. In a unit test, hand in a FakePaymentGateway instead of a real one. No mocking framework needed.
  • Swappability. Switch from Razorpay to Stripe by changing a configuration line, not 200 source files.
  • Lifecycle management. The container decides when to create, share, and destroy beans โ€” singleton vs request-scoped, eager vs lazy, etc.
  • Cross-cutting concerns. Because beans go through the container, Spring can wrap them with proxies for transactions, security, caching, async โ€” without your code knowing.
IoC is the principle ("don't let classes wire themselves"); DI is the technique ("inject dependencies through the constructor"). Spring's container is just an industrial-scale implementation of DI that also gives you proxies, lifecycles, and config on top.
When asked "what is Spring?", lead with: "It's a container that creates objects, wires their dependencies, and wraps them with cross-cutting concerns like transactions and security." Then show the bad-code โ†’ good-code comparison. Skip the textbook tour.
02

ApplicationContext vs BeanFactory โ€” Two Containers, One Lineage

What's the difference between BeanFactory and ApplicationContext? Which one does Spring Boot use?

The two interfaces

  • BeanFactory โ€” the original, low-level container. Lazy by default (creates beans only when first asked). Knows about beans and their wiring, nothing more.
  • ApplicationContext โ€” the grown-up superset. Extends BeanFactory and adds: event publishing, internationalization, environment abstraction, AOP integration, eager singleton instantiation by default, and resource loading.
BeanFactory is a vending machine โ€” drop in the bean name, get a bean. ApplicationContext is the whole shop floor โ€” vending machines plus a PA system (events), price boards (Environment), translators (i18n), and a stockroom that pre-fills hot items (eager singletons).

Common ApplicationContext implementations

ClassUsed for
AnnotationConfigApplicationContextJava config + annotations (modern default)
ClassPathXmlApplicationContextClassic XML config (legacy)
AnnotationConfigServletWebServerApplicationContextWhat Spring Boot uses for embedded Tomcat
ReactiveWebServerApplicationContextWebFlux apps
Spring Boot uses ApplicationContext, not raw BeanFactory. You almost never need to drop down to BeanFactory in modern code โ€” and if an interviewer hears you reach for it casually, they'll probe.
03

Bean Scopes โ€” One Bean, Many Lifetimes

Maya builds a UserContext bean to hold "the currently logged-in user." She marks it @Component. In production, two users start seeing each other's data. Why? She accidentally left the bean as a singleton โ€” one shared instance across every request and every thread.
List the bean scopes in Spring. When would you use each?

The five built-in scopes

ScopeLifetimeWhen to use
singleton (default)One per Spring containerStateless services, repositories, controllers, gateways
prototypeNew instance every getBean()Stateful objects you build per-task (rare)
requestOne per HTTP requestPer-request data (request id, user context)
sessionOne per HTTP sessionShopping cart, session preferences
applicationOne per ServletContextRare โ€” mostly equivalent to singleton in modern apps
Declaring a non-default scope
@Component
@Scope("prototype")
class ReportBuilder { /* fresh instance per request */ }

@Component
@RequestScope
class UserContext { private String userId; }
The classic trap: injecting a prototype bean into a singleton. The singleton holds onto the first prototype instance forever โ€” defeating the point. Fix: inject ObjectProvider<ReportBuilder> or Provider<ReportBuilder> and call .getObject() each time, or annotate the prototype with @Scope(value="prototype", proxyMode=ScopedProxyMode.TARGET_CLASS).
Singleton is the office water cooler โ€” one cooler shared by everyone. Prototype is a paper cup โ€” fresh one each time. Request is a visitor badge โ€” issued at the door, shredded when you leave. Session is a hotel keycard โ€” works for the duration of your stay.
"Why is singleton the default?" โ€” Because most beans (controllers, services, repositories) are stateless. Stateless + shared = no thread-safety issue + maximum reuse + no GC churn.
04

Bean Lifecycle โ€” From Class on Disk to Object on the Heap

Walk me through the lifecycle of a Spring bean.

The full sequence (singleton scope)

  1. Class detection. Component scan finds @Component, @Service, @Configuration, etc., or you declare a @Bean method.
  2. Bean definition registered. Spring stores metadata (class, scope, dependencies) in a BeanDefinition.
  3. Instantiation. Spring calls the constructor, passing in any constructor-injected dependencies.
  4. Property population. Setters and field injection happen.
  5. Aware callbacks. If the bean implements BeanNameAware, ApplicationContextAware, etc., those are called.
  6. BeanPostProcessor.postProcessBeforeInitialization() โ€” registered post-processors get a shot. AOP proxy creation happens here.
  7. Initialization. @PostConstruct, then InitializingBean.afterPropertiesSet(), then any custom init-method.
  8. BeanPostProcessor.postProcessAfterInitialization() โ€” final wrapping.
  9. Bean in use. Application code injects/uses the bean.
  10. Container shutdown. @PreDestroy, then DisposableBean.destroy(), then any custom destroy-method.
The hooks you'll actually use
@Component
class ConnectionPool {

    @PostConstruct
    public void warmUp() {
        // open connections, prefetch metadata, etc.
    }

    @PreDestroy
    public void drain() {
        // close connections cleanly
    }
}
Bean lifecycle is like onboarding at a new company. You're hired (instantiated), HR fills out your forms (property population), you're told your manager's name (Aware callbacks), security badges you in (post-processors), you sit through orientation (@PostConstruct), and you start working. On your last day, you do an exit interview (@PreDestroy) and hand back the laptop.
@PostConstruct won't be called if the bean is wrapped by an AOP proxy in a way that breaks reflection on the original class โ€” and starting from Java 9, @PostConstruct moved out of the JDK into jakarta.annotation. Spring Boot 3 uses jakarta.*; older 2.x uses javax.*. Importing the wrong one means the annotation silently does nothing.
05

@Component, @Service, @Repository, @Controller โ€” Same Family, Different Roles

Are @Service and @Component the same? Why do we have multiple stereotypes?

Functionally? Mostly the same. Semantically? Different roles.

All four register the class as a Spring bean. The differences are intent signaling + a few specific behaviors:

  • @Component โ€” generic Spring-managed bean. The base annotation; the others are specializations.
  • @Service โ€” marker for the service layer (business logic). Same as @Component at runtime, but tells readers "this is where the logic lives."
  • @Repository โ€” marker for the persistence layer. Spring auto-translates JDBC/JPA exceptions into DataAccessException โ€” that's a real behavioral difference.
  • @Controller โ€” marker for an MVC controller. Spring MVC's DispatcherServlet looks for these to dispatch HTTP requests.
  • @RestController = @Controller + @ResponseBody โ€” every method's return value is serialized to JSON, no view resolution.
They're all "employees of the company" (beans). The job title (Service / Repository / Controller) tells everyone โ€” including Spring โ€” what role you play. A "Repository" employee has a special insurance perk (exception translation); a "Controller" gets routed customer calls (HTTP requests). The work title isn't pure decoration โ€” it changes which corporate systems integrate with you.
"Can I just use @Component everywhere?" Technically yes โ€” but you lose the exception translation on repositories, and grep'ing your codebase for "where's the business logic?" becomes harder. Use the right stereotype.
If asked "is there any difference at runtime?" โ€” say "Yes, @Repository enables persistence-exception translation; @Controller hooks into MVC routing. The others are equivalent to @Component functionally, but communicate intent." That answer beats both extremes.
06

Constructor vs Setter vs Field Injection โ€” Pick One and Don't Look Back

A junior dev opens a 4-year-old service class. It has 17 fields, all @Autowired. He has no idea which dependencies are required, which are optional, which are circular. He can't write a unit test without firing up Spring. This is the cost of field injection.
Which injection type should you use? Why is field injection considered an anti-pattern?

The three styles

All three side-by-side
// 1. Constructor injection โ€” the recommended way
@Service
class CheckoutService {
    private final PaymentGateway gw;
    private final OrderRepository orders;

    public CheckoutService(PaymentGateway gw, OrderRepository orders) {
        this.gw = gw;
        this.orders = orders;
    }
}

// 2. Setter injection โ€” for optional dependencies
@Service
class EmailService {
    private MetricsClient metrics;

    @Autowired(required = false)
    public void setMetrics(MetricsClient m) { this.metrics = m; }
}

// 3. Field injection โ€” convenient, but considered an anti-pattern
@Service
class UserService {
    @Autowired
    private UserRepository repo;
}

Why constructor injection wins

  • Final fields = immutability. Compiler enforces that dependencies can't change after construction.
  • No partially constructed objects. Object exists only when fully wired.
  • Mandatory dependencies are obvious. Anything in the constructor is required; setters are clearly optional.
  • Trivial to unit test. new CheckoutService(fakeGw, fakeRepo). No Spring, no reflection.
  • Circular dependencies fail loudly at startup, not silently at runtime.
  • Since Spring 4.3, the @Autowired annotation is optional if the class has only one constructor.
Constructor injection is "I refuse to walk on stage without my microphone, my script, and my water." Setter injection is "I'll start without water; bring it later if you can." Field injection is "I'll perform; somebody invisible will hand me props through a hidden hatch โ€” pray they show up."
Field injection breaks immutability (the field can't be final), hides dependencies (caller can't tell what the bean needs), couples you to Spring for testing (no reflection magic, no test), and lets cycles slip in unnoticed.
If a constructor has too many parameters, that's a code smell โ€” the class has too many responsibilities. Don't fix it by switching to field injection. Fix it by splitting the class. Constructor pain = SRP violation pointing at itself.
07

When Spring Has Two Beans of the Same Type โ€” @Qualifier, @Primary, @Profile

Anvi defines a RazorpayGateway and a StripeGateway โ€” both implement PaymentGateway. She injects PaymentGateway into a service. Spring throws NoUniqueBeanDefinitionException: "two beans, one slot, you pick."

Three ways to disambiguate

@Qualifier โ€” name-based pick
@Service
class CheckoutService {
    public CheckoutService(@Qualifier("stripeGateway") PaymentGateway gw) {
        // Spring picks the bean named "stripeGateway"
    }
}
@Primary โ€” default winner when ambiguous
@Component
@Primary
class RazorpayGateway implements PaymentGateway { }

@Component
class StripeGateway implements PaymentGateway { }

// Anywhere PaymentGateway is injected without @Qualifier โ†’ RazorpayGateway wins
@Profile โ€” pick by environment
@Component
@Profile("prod")
class RazorpayGateway implements PaymentGateway { }

@Component
@Profile({"dev", "test"})
class FakeGateway implements PaymentGateway { }

// Switched via spring.profiles.active=dev or =prod

Combining them โ€” the resolution order

  1. Profile filtering happens first โ€” beans for inactive profiles aren't even registered.
  2. If exactly one bean of the type is registered โ†’ injected.
  3. Multiple โ†’ @Qualifier if present picks one; else @Primary picks one; else error.
For multi-tenant or multi-region apps, prefer @Profile over runtime if/else โ€” it keeps the wrong bean out of the context entirely, which is faster and prevents accidental usage.
08

Auto-configuration โ€” How Spring Boot Reads Your Mind

Karthik adds spring-boot-starter-data-jpa and com.h2database:h2 to a fresh project. He writes one @Entity and one JpaRepository โ€” and just like that, his app starts up with an in-memory database, schema, and a fully-wired DataSource. He never wrote a @Bean. How?
How does Spring Boot's auto-configuration actually work? What's @EnableAutoConfiguration?

The mechanism, in three layers

  1. @SpringBootApplication is a meta-annotation = @Configuration + @ComponentScan + @EnableAutoConfiguration.
  2. @EnableAutoConfiguration imports AutoConfigurationImportSelector, which scans every JAR for META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (Spring Boot 3+) โ€” a plain text file listing config classes to import.
  3. Each listed config class is gated with conditions:
    • @ConditionalOnClass โ€” only apply if a class is on the classpath.
    • @ConditionalOnMissingBean โ€” only register if the user hasn't already defined one.
    • @ConditionalOnProperty โ€” only if a property is set.
    • @ConditionalOnWebApplication โ€” only in web apps.
A real auto-config snippet (simplified)
@AutoConfiguration
@ConditionalOnClass(DataSource.class)
@ConditionalOnMissingBean(DataSource.class)
class DataSourceAutoConfiguration {

    @Bean
    @ConfigurationProperties("spring.datasource")
    public DataSource dataSource() { /* HikariCP wired with props */ }
}
Auto-config is like a smart room. You walk in, and based on what you brought (jars on the classpath), the room turns on the right lights (DataSource if H2 is present), starts the AC if it's hot (server config if a web starter is present), and steps back if you bring your own (you defined a DataSource bean โ†’ Spring's stays out of the way).

Three flags every interviewer will probe

  • spring.autoconfigure.exclude=... in properties โ€” disable specific auto-configs.
  • @EnableAutoConfiguration(exclude = ...) โ€” same thing, in code.
  • --debug on startup โ€” prints the "auto-configuration report": what was matched, what was rejected and why.
Auto-configuration is opinionated default wiring with override hooks. It's not magic โ€” it's a curated list of conditional @Configuration classes that only fire when the situation matches and the user hasn't already taken over.
09

Starters & The POM โ€” Why You Don't Manage Versions Anymore

What's a Spring Boot starter? Why do you not specify versions in your POM?

Starters in one sentence

A starter is a curated dependency descriptor (a JAR with no code, only a POM) that pulls in everything you need for a feature. spring-boot-starter-web brings in Spring MVC + Tomcat + Jackson + Logback โ€” one dependency, six bundled.

The version magic โ€” spring-boot-starter-parent

Your project inherits from spring-boot-starter-parent, which inherits from spring-boot-dependencies โ€” a giant BOM (Bill of Materials) that specifies the exact compatible version of every Spring-and-related library. So when you write:

No version needed
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <!-- <version> intentionally omitted -->
</dependency>

Maven looks up the version from the BOM. The whole point: stop the "library X 4.2 needs library Y 1.6 but Spring needs Y 1.5" hell that Java was famous for.

Common starters worth memorizing

StarterBrings in
spring-boot-starter-webMVC + embedded Tomcat + Jackson
spring-boot-starter-webfluxReactive web (Netty by default)
spring-boot-starter-data-jpaHibernate + Spring Data JPA + HikariCP
spring-boot-starter-data-redisLettuce + Spring Data Redis
spring-boot-starter-securitySpring Security core
spring-boot-starter-validationHibernate Validator (JSR-380)
spring-boot-starter-actuatorHealth, metrics, info endpoints
spring-boot-starter-testJUnit 5, Mockito, AssertJ, Spring Test
If you don't want to inherit spring-boot-starter-parent (e.g., your company has its own corporate parent POM), import spring-boot-dependencies as a BOM in <dependencyManagement>. You get the same version curation without the parent.
10

application.yml, @Value, and @ConfigurationProperties

Difference between @Value and @ConfigurationProperties? When would you use which?

The two ways to read config

@Value โ€” single property
@Component
class RetryService {
    @Value("${retry.max-attempts:3}")
    private int maxAttempts;   // 3 is the default if the property is missing
}
@ConfigurationProperties โ€” type-safe, structured
@Component
@ConfigurationProperties(prefix = "retry")
class RetryConfig {
    private int maxAttempts = 3;
    private Duration backoff = Duration.ofMillis(200);
    private List<String> retryableErrors;
    // getters and setters omitted
}

Now in application.yml:

application.yml
retry:
  max-attempts: 5
  backoff: 500ms
  retryable-errors:
    - TIMEOUT
    - CONNECTION_RESET

When to use which

UseWhen
@ValueOne or two scalar properties, simple injection
@ConfigurationPropertiesA group of related properties, nested objects, lists, validation, refresh support

Property source precedence (high to low)

  1. Command-line args (--server.port=9000)
  2. SPRING_APPLICATION_JSON environment variable
  3. OS environment variables
  4. External application-<profile>.yml
  5. Classpath application-<profile>.yml
  6. External application.yml
  7. Classpath application.yml
  8. @PropertySource annotations
  9. Defaults (SpringApplication.setDefaultProperties)
Properties from higher sources override lower ones. This is why a CI pipeline can override DB credentials by setting environment variables โ€” without changing any YAML.
Add @Validated to your @ConfigurationProperties class and JSR-380 annotations on fields (@NotNull, @Min) to fail-fast at startup if config is wrong. A typo in production should never reach a request.
11

Profiles โ€” One Codebase, Many Environments

The basics

A profile is a named slice of configuration. You write per-profile YAMLs (application-dev.yml, application-prod.yml) and per-profile beans (@Profile("dev")). Spring activates a profile via spring.profiles.active.

Activating a profile โ€” three ways
// 1. application.yml
spring:
  profiles:
    active: dev

// 2. JVM arg
-Dspring.profiles.active=prod

// 3. Environment variable
SPRING_PROFILES_ACTIVE=prod

Multi-document YAML (Spring Boot 2.4+)

One file, three slices
spring:
  application:
    name: checkout
---
spring:
  config:
    activate:
      on-profile: dev
datasource:
  url: jdbc:h2:mem:test
---
spring:
  config:
    activate:
      on-profile: prod
datasource:
  url: ${PROD_DB_URL}
  username: ${PROD_DB_USER}
A profile is additive, not exclusive. spring.profiles.active=dev,monitoring activates both. @Profile("!prod") means "register when prod is NOT active" โ€” useful for dev-only beans.
Never put real credentials in version-controlled YAML โ€” even in non-prod profiles. Use environment variables, a secret manager (AWS Secrets Manager, Vault), or Spring Cloud Config Server.
12

The Embedded Server & main() โ€” Why Spring Boot Apps Are Just JARs

Why does Spring Boot have an embedded server? How does the JAR run a web app without a WAR or Tomcat install?

The shift from WAR to executable JAR

Pre-Boot, you wrote a WAR and deployed it to an external Tomcat / WebSphere / WildFly. Boot inverted this: the server lives inside your app. spring-boot-starter-web bundles Tomcat as a library; the main() method bootstraps it.

The whole web app, in nine lines
@SpringBootApplication
public class CheckoutApp {
    public static void main(String[] args) {
        SpringApplication.run(CheckoutApp.class, args);
    }
}

What SpringApplication.run() does

  1. Creates an ApplicationContext (which one depends on classpath: web vs reactive vs plain).
  2. Loads property sources, activates profiles.
  3. Runs auto-configuration โ†’ registers beans.
  4. Component scans starting from the package of the class with @SpringBootApplication.
  5. Calls refresh() on the context โ€” instantiates singletons, runs @PostConstruct, starts embedded server.
  6. Calls any CommandLineRunner / ApplicationRunner beans.

Switching the server

Tomcat โ†’ Jetty
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
Old-school Java apps were like buying a stove (Tomcat) separately from your kitchen ingredients (your app). Spring Boot is the food truck โ€” stove, prep table, fridge, cashier, all welded into a single self-contained vehicle. You ship the truck, not just the menu.
For Kubernetes deployments, Spring Boot's executable JAR is gold โ€” one artifact, one process, predictable startup. The "build once, run anywhere" promise actually works.
13

DispatcherServlet โ€” The Front Door for Every HTTP Request

Walk me through what happens between an HTTP request hitting your app and your @GetMapping method being called.

The flow, step by step

  1. Tomcat (or your server) accepts the TCP connection, parses HTTP, hands a HttpServletRequest to the registered servlet.
  2. Spring Boot registered DispatcherServlet (mapped to /) โ€” it gets the request.
  3. HandlerMapping (typically RequestMappingHandlerMapping) looks up the matching @RequestMapping method by URL + method + headers + content type.
  4. HandlerAdapter (RequestMappingHandlerAdapter) invokes that method:
    • Resolves arguments via HandlerMethodArgumentResolvers (@PathVariable, @RequestBody, @RequestParam, etc.).
    • Calls your method.
    • Handles the return value via HandlerMethodReturnValueHandlers.
  5. If @ResponseBody (or @RestController) โ€” the return value is serialized via an HttpMessageConverter (Jackson for JSON, etc.).
  6. If a view name is returned โ€” the ViewResolver resolves it to a view (Thymeleaf, JSP).
  7. Any exception โ†’ caught by HandlerExceptionResolver chain, which finds the matching @ExceptionHandler or @ControllerAdvice.
  8. Response written back through Tomcat to the client.
DispatcherServlet is the receptionist of a giant office building. The visitor (request) walks in. The receptionist looks up a directory (HandlerMapping) โ€” "Aha, you want HR โ€” that's room 3B, Maya handles benefits." She walks the visitor over (HandlerAdapter), Maya answers, the receptionist brings the answer back, prints it on letterhead (HttpMessageConverter), hands it to the visitor, and updates the visitor log.
Filters run before DispatcherServlet (servlet container level). Interceptors run inside DispatcherServlet (after handler resolution). Choosing the wrong layer for cross-cutting logic (auth, logging) is a common bug.
14

REST Controller Anatomy โ€” All the Annotations You Actually Need

A "what to memorize" example
@RestController
@RequestMapping("/api/v1/orders")
class OrderController {

    private final OrderService svc;
    public OrderController(OrderService svc) { this.svc = svc; }

    @GetMapping("/{id}")
    public OrderDto getOne(@PathVariable String id) {
        return svc.findById(id);
    }

    @GetMapping
    public Page<OrderDto> list(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestHeader("X-Tenant-Id") String tenant) {
        return svc.list(tenant, page, size);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public OrderDto create(@Valid @RequestBody CreateOrderRequest req) {
        return svc.create(req);
    }

    @PutMapping("/{id}")
    public ResponseEntity<OrderDto> update(
            @PathVariable String id,
            @RequestBody UpdateOrderRequest req) {
        OrderDto updated = svc.update(id, req);
        return ResponseEntity.ok().eTag(updated.version()).body(updated);
    }

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

Annotations cheatsheet

AnnotationPurpose
@RestController@Controller + @ResponseBody on every method
@RequestMappingClass-level URL prefix (or method-level catch-all)
@GetMapping / @PostMapping / @PutMapping / @PatchMapping / @DeleteMappingMethod-level shortcuts
@PathVariableExtracts URL segments (/orders/{id})
@RequestParamQuery string params (?page=0)
@RequestBodyDeserialize HTTP body into a Java object (Jackson)
@RequestHeaderRead a header value
@ResponseBodySerialize return value (already implied by @RestController)
@ResponseStatusOverride default 200 with a status (201, 204, etc.)
ResponseEntity<T>Programmatic control: status + headers + body

When to return ResponseEntity vs a plain DTO

  • Plain DTO โ†’ simplest, fine for 99% of GETs.
  • ResponseEntity โ†’ when you need custom headers (ETag, Location), conditional status, or a body that depends on logic (200 vs 304).
"Why isn't my JSON body deserialized?" โ€” Most often: missing default constructor + setters on the DTO (Jackson needs them), or you forgot @RequestBody on the parameter, or content-type isn't application/json. Use Java records (Spring Boot 2.6+) โ€” they work natively with Jackson.
15

Exception Handling โ€” @ControllerAdvice, @ExceptionHandler, ProblemDetail

Sarah's API throws random 500s when input is bad. The frontend devs are furious โ€” "Just give me a sane error JSON, I don't need a stack trace in the body." Sarah needs centralized exception handling.
How do you handle exceptions globally in Spring Boot?

The pattern โ€” global advice

@ControllerAdvice in action
@RestControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(EntityNotFoundException.class)
    public ResponseEntity<ProblemDetail> notFound(EntityNotFoundException ex) {
        ProblemDetail p = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(p);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> validation(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors()
          .forEach(e -> errors.put(e.getField(), e.getDefaultMessage()));
        return ResponseEntity.badRequest().body(errors);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ProblemDetail> fallback(Exception ex) {
        log.error("Unhandled", ex);
        return ResponseEntity.status(500)
            .body(ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "Something went wrong"));
    }
}

ProblemDetail โ€” RFC 7807 (Spring 6+)

Spring 6 / Boot 3 ship ProblemDetail, a built-in type that follows RFC 7807 (Problem Details for HTTP APIs). One standard error shape across services beats every team inventing their own.

Sample ProblemDetail JSON
{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "Order 12345 not found",
  "instance": "/api/v1/orders/12345"
}
@ControllerAdvice applies to ALL controllers by default. To scope it: @ControllerAdvice(basePackages = "com.example.api.v2") or @ControllerAdvice(annotations = AdminApi.class).
Order matters: more specific exception classes win over broader ones. A handler for EntityNotFoundException takes precedence over one for RuntimeException. Spring resolves this automatically by class hierarchy.
16

Bean Validation โ€” @Valid, @Validated, JSR-380

The annotations you'll use 95% of the time

AnnotationValidates
@NotNullNot null
@NotBlankString not null and not whitespace-only
@NotEmptyCollection / array / string not empty
@Size(min, max)String / collection length
@Min / @MaxNumeric bounds
@EmailRFC-style email format
@Pattern(regexp)Regex match
@Past / @FutureDate/time bounds
Validation in action
record CreateOrderRequest(
        @NotBlank String userId,
        @NotEmpty List<@Valid LineItem> items,
        @Min(1) int quantity
) {}

@PostMapping
public OrderDto create(@Valid @RequestBody CreateOrderRequest req) {
    // if validation fails, MethodArgumentNotValidException is thrown
    // โ†’ 400 Bad Request, handled by @ControllerAdvice
}

@Valid vs @Validated

  • @Valid โ€” JSR-380 standard. Triggers nested validation.
  • @Validated โ€” Spring's variant. Adds group support (validate different rules at different times) and works on method parameters in any Spring bean (not just controllers).

Custom constraint

Validate against a custom rule
@Constraint(validatedBy = SkuValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidSku {
    String message() default "invalid SKU";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

class SkuValidator implements ConstraintValidator<ValidSku, String> {
    public boolean isValid(String sku, ConstraintValidatorContext ctx) {
        return sku != null && sku.matches("[A-Z]{3}-\\d{4}");
    }
}
Without spring-boot-starter-validation on the classpath, @Valid on a controller parameter silently does nothing. Spring Boot 2.3+ removed it from spring-boot-starter-web โ€” you must add it explicitly.
17

Filters, Interceptors, and AOP โ€” Three Layers of Cross-Cutting Concerns

When would you use a filter vs an interceptor vs an aspect?

The three layers, outside-in

LayerWhere it sitsSeesUse for
Servlet FilterServlet container, before DispatcherServletRaw request/response, no controller infoLogging, GZIP, CORS, basic auth
HandlerInterceptorInside DispatcherServlet, after handler resolutionThe matched handler methodMDC setup, auth checks per route, timing
AOP AspectAround any Spring bean methodMethod args, return value, exceptionsTransactions, caching, retries, custom logging
A simple aspect
@Aspect
@Component
class TimingAspect {

    @Around("@annotation(Timed)")
    public Object time(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        try {
            return pjp.proceed();
        } finally {
            log.info("{} took {}ms", pjp.getSignature(),
                (System.nanoTime() - start) / 1_000_000);
        }
    }
}

How Spring AOP actually works

Spring AOP is proxy-based. When the container creates a bean that has methods to advise, it wraps the bean in a proxy object. Callers get the proxy. The proxy intercepts every method call, runs the advice, and delegates to the real method.

  • If the bean implements an interface โ†’ Spring uses a JDK dynamic proxy.
  • If not โ†’ Spring uses CGLIB (subclass proxy).
The most common AOP bug: self-invocation doesn't trigger advice. Calling this.foo() from another method in the same bean bypasses the proxy. So @Transactional, @Cacheable, and your custom advice all silently no-op when called internally. Fix: call through an injected reference, or use AspectJ load-time weaving for cross-cutting at the bytecode level.
A proxy is like a personal assistant sitting in front of your CEO's office. Visitors talk to the assistant, the assistant decides what to log/charge/block, then opens the door to the CEO. But if the CEO calls a colleague from inside the office, the assistant never sees it โ€” that's self-invocation.
18

Spring Data JPA โ€” The Repository Pattern, Crystallized

Raj writes 200 lines of JDBC boilerplate to load a User by email. The next day, Karthik finishes the same task in 4 lines: declare an interface, write one method signature, done. Raj wonders if Karthik is using witchcraft. He's using Spring Data JPA.
How does Spring Data JPA generate query implementations from interface method names?

The repository hierarchy

  • Repository<T, ID> โ€” marker, no methods.
  • CrudRepository<T, ID> โ€” adds save, findById, delete, count.
  • PagingAndSortingRepository<T, ID> โ€” adds Page<T> findAll(Pageable).
  • JpaRepository<T, ID> โ€” adds JPA-specific bits like flush(), saveAndFlush(), batch ops.
Derived queries โ€” method name = SQL
interface UserRepository extends JpaRepository<User, Long> {

    // SELECT * FROM users WHERE email = ?
    Optional<User> findByEmail(String email);

    // SELECT * FROM users WHERE active = true ORDER BY created_at DESC
    List<User> findByActiveTrueOrderByCreatedAtDesc();

    // SELECT COUNT(*) FROM users WHERE org_id = ?
    long countByOrgId(String orgId);

    // Custom JPQL when name parsing isn't enough
    @Query("select u from User u where u.email like :pattern")
    List<User> searchByEmail(@Param("pattern") String pattern);

    // Native SQL when JPQL isn't enough
    @Query(value = "SELECT * FROM users WHERE created_at > NOW() - INTERVAL '7 days'",
            nativeQuery = true)
    List<User> recentlyCreated();
}

How method names become queries

Spring Data parses your method name at startup using a state machine: findBy + property + (operator) + connector + property. Operators include And, Or, Like, StartingWith, GreaterThan, Between, In, IsNull. If the parser can't resolve a property, the app fails fast at startup โ€” not at runtime.

Pagination & sorting

Page and Sort
Pageable page = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<User> result = repo.findAll(page);

result.getContent();         // the items
result.getTotalElements();   // total count (extra COUNT query)
result.getTotalPages();      // total pages
result.hasNext();            // more to come?
Page issues an extra COUNT(*) query on every call โ€” fine for small tables, expensive for huge ones. For "infinite scroll" UIs, return Slice<T> instead โ€” same data, no count query.
19

@Transactional โ€” The Annotation Most Devs Get Wrong

How does @Transactional work under the hood? What are propagation and isolation levels?

The mechanics

Spring wraps your bean in an AOP proxy. When you call an @Transactional method, the proxy:

  1. Asks the PlatformTransactionManager for a transaction (start a new one or join an existing one based on propagation).
  2. Invokes your method.
  3. If your method completes normally โ†’ commit.
  4. If your method throws an unchecked exception (RuntimeException) or Error โ†’ rollback.
  5. Checked exceptions โ†’ commit (unless you set rollbackFor).

Propagation โ€” what to do when a transaction already exists

ValueBehavior
REQUIRED (default)Join existing or start new
REQUIRES_NEWAlways suspend existing, start new (commits independently)
SUPPORTSRun inside if exists, otherwise non-transactional
MANDATORYMust be inside an existing transaction (else error)
NEVERMust NOT be inside a transaction
NESTEDSavepoint-based nested transaction (DB support required)

Isolation โ€” what dirty reads / phantoms / etc. you allow

LevelPrevents
READ_UNCOMMITTEDNothing โ€” sees uncommitted writes
READ_COMMITTEDDirty reads (default in PostgreSQL, Oracle)
REPEATABLE_READDirty + non-repeatable reads (default in MySQL InnoDB)
SERIALIZABLEEverything โ€” including phantom reads (slowest)

Common bugs

Self-invocation kills @Transactional. A method calls another method on this โ†’ AOP proxy bypassed โ†’ no transaction. Fix: call via an injected self-reference or split into two beans.
Checked exceptions don't roll back by default. throw new IOException("...") commits the transaction. Add @Transactional(rollbackFor = Exception.class) if that's what you want.
@Transactional on private methods is a no-op. Spring AOP proxies only work on public methods (CGLIB can do package-private but it's flaky). The annotation is silently ignored.
Long-running transactions hurt connection pools. A @Transactional method that does I/O (HTTP calls, sleeps) holds a DB connection the whole time. Pool exhausts under load. Keep transactions short; do non-DB work outside them.
@Transactional(readOnly = true) on read-only services is a free win โ€” Hibernate skips dirty checking, and some pools route to read replicas.
20

N+1, Lazy Loading, and the FetchType Trap

Priya's "/orders" endpoint loads 100 orders in 2.3 seconds. The DB shows 101 queries: 1 to list orders + 100 to load each order's customer. She's met the N+1 problem โ€” the most-asked Hibernate question in interviews.
What's the N+1 problem in JPA? How do you fix it?

The setup

A typical mapping that causes N+1
@Entity
class Order {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;
    // ...
}

// Then:
List<Order> orders = repo.findAll();      // 1 query
for (Order o : orders) {
    String name = o.getCustomer().getName();   // triggers 1 query per order!
}

Result: 1 + N queries. With 1000 orders, 1001 round-trips to the DB.

Three fixes

1. JOIN FETCH
@Query("select o from Order o join fetch o.customer")
List<Order> findAllWithCustomer();
2. EntityGraph (declarative)
@EntityGraph(attributePaths = {"customer", "items"})
List<Order> findAll();
3. Hibernate batch fetching (global tuning)
# in application.yml
spring.jpa.properties.hibernate.default_batch_fetch_size: 50

# Now Hibernate loads up to 50 customers in ONE query: WHERE customer_id IN (...)

FetchType โ€” which default to pick

  • FetchType.LAZY โ€” load when accessed. Default for @OneToMany / @ManyToMany.
  • FetchType.EAGER โ€” load with the parent. Default for @ManyToOne / @OneToOne.
EAGER is the silent killer. A "small" entity with EAGER associations can pull half the database into memory with a single findById. Make everything LAZY by default; opt into eager loading per-query with JOIN FETCH or EntityGraph.
LazyInitializationException happens when you access a lazy field outside a transaction or session โ€” the entity is "detached" and can't reach back to the DB. Common with controllers returning entities directly. Fix: use DTOs, fetch what you need inside the transaction, or use Open-Session-In-View (controversial โ€” better to fix the root cause).
21

Caching โ€” @Cacheable, @CacheEvict, and Why Your Cache Lies

The annotations

Caching basics
@Service
class UserService {

    @Cacheable(value = "users", key = "#id")
    public User findById(String id) {
        return repo.findById(id).orElseThrow();
    }

    @CachePut(value = "users", key = "#user.id")
    public User update(User user) {
        return repo.save(user);   // run AND update cache
    }

    @CacheEvict(value = "users", key = "#id")
    public void delete(String id) { repo.deleteById(id); }

    @CacheEvict(value = "users", allEntries = true)
    public void clearAll() { }
}

And on the application class:

Enable + pick a provider
@SpringBootApplication
@EnableCaching
class App { }

# Add to pom.xml for Caffeine (in-memory) or Redis (distributed):
# spring-boot-starter-cache + caffeine, or + spring-boot-starter-data-redis

Cache pitfalls

Self-invocation again. Same as @Transactional โ€” calling a cached method from another method in the same bean bypasses the proxy and skips the cache.
Stale data after writes through other paths. If anything updates the DB outside your cached service (another service, a script, another node) the cache is now lying. Either centralize all writes through the same service or use cache-aside with TTL.
Caching null silently breaks. By default, Spring caches null returns โ€” so a findById on a non-existent key gets cached as null forever (or until eviction). Use unless = "#result == null" to skip caching nulls.
For distributed caches (Redis), @Cacheable auto-serializes the return value. Make sure your DTOs are Serializable or use Jackson serializers โ€” and never cache mutable entity proxies (Hibernate HibernateProxy mid-session โ€” boom).
22

@Async and @Scheduled โ€” Background Work, Made Easy

Both in action
@SpringBootApplication
@EnableAsync
@EnableScheduling
class App { }

@Service
class EmailService {

    @Async
    public CompletableFuture<Void> sendWelcome(User u) {
        // runs on a different thread; returns immediately to caller
        smtp.send(u.email, "Welcome!");
        return CompletableFuture.completedFuture(null);
    }
}

@Component
class CleanupJob {

    @Scheduled(cron = "0 0 3 * * *")         // every day at 3 AM
    public void deleteOldOrders() { ... }

    @Scheduled(fixedDelay = 10_000)          // 10s after previous run finishes
    public void heartbeat() { ... }

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

@Async โ€” what to know

  • Methods can return void, CompletableFuture<T>, or ListenableFuture<T>.
  • Same self-invocation rule โ€” calling this.someAsyncMethod() from within the same bean runs synchronously.
  • By default, uses SimpleAsyncTaskExecutor โ€” creates a new thread per call. Replace it: define a TaskExecutor bean named taskExecutor or a ThreadPoolTaskExecutor bean.

@Scheduled โ€” what to know

  • Default thread pool is single-threaded โ€” long-running jobs delay other jobs. Add a ThreadPoolTaskScheduler bean for parallelism.
  • Cron uses Spring's 6-field syntax (second minute hour day-of-month month day-of-week) โ€” different from Linux cron's 5 fields.
  • In a multi-instance deployment, every instance fires the schedule. For "run only once across the cluster," use ShedLock or quartz.
"My @Async method runs synchronously" โ€” Most likely either self-invocation, missing @EnableAsync, or the method is private/final.
23

Spring Security โ€” Filter Chain, Authentication, Authorization

How does Spring Security work? What's the filter chain?

The big picture

Spring Security is a chain of servlet filters that intercepts every request before it reaches your controller. Each filter has one job: parse a JWT, check a session, authorize a route, etc. The chain ends at FilterSecurityInterceptor, which decides authorize-or-deny.

  1. SecurityContextPersistenceFilter โ€” load existing auth from session, store at end.
  2. UsernamePasswordAuthenticationFilter โ€” for form login posts.
  3. BasicAuthenticationFilter โ€” for HTTP Basic.
  4. BearerTokenAuthenticationFilter โ€” for JWTs (OAuth2 resource server).
  5. ExceptionTranslationFilter โ€” converts security exceptions into HTTP responses.
  6. FilterSecurityInterceptor โ€” the final authorization gate.

Modern config (Spring Security 6+)

SecurityFilterChain bean
@Configuration
@EnableWebSecurity
class SecurityConfig {

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

Authentication vs Authorization

  • Authentication โ€” "who are you?" Verifies credentials, produces an Authentication object.
  • Authorization โ€” "what are you allowed to do?" Checks the principal's authorities/roles against the resource's requirements.

Method-level security

@PreAuthorize and friends
@EnableMethodSecurity
class SecurityConfig { }

@Service
class OrderService {
    @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
    public List<Order> forUser(String userId) { ... }
}
CSRF is enabled by default in Spring Security. For pure REST APIs (stateless, JWT-based), disable it. For browser-rendered apps, leave it on and emit a token.
24

JWT & OAuth2 โ€” Stateless Auth Without the Pain

JWT in 60 seconds

A JWT is three Base64-encoded parts joined by dots: header.payload.signature. The header says the algorithm; the payload holds claims (subject, expiry, custom data); the signature proves the token wasn't tampered with.

  • Stateless โ€” server doesn't store sessions; the token IS the session.
  • Self-contained โ€” every claim the API needs is in the token (user id, roles, expiry).
  • Verifiable โ€” server validates signature with a known key (HMAC) or public key (RSA/EC).

The Spring Security 6 setup

JWT resource server
# pom.xml: spring-boot-starter-oauth2-resource-server

# application.yml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com
          # Spring auto-discovers /.well-known/openid-configuration
          # and pulls the JWKS endpoint to verify signatures

That's it. Spring registers the right filter, validates each incoming JWT, and exposes the principal as Jwt or your custom converter's output.

OAuth2 โ€” the four roles

RoleWho
Resource OwnerThe user (you)
ClientThe app asking for access
Authorization ServerIssues tokens (Auth0, Okta, Cognito)
Resource ServerYour API โ€” validates tokens, serves data

The grant flows

  • Authorization Code (with PKCE) โ€” for SPAs and mobile apps. The modern default.
  • Client Credentials โ€” for service-to-service (no user involved).
  • Resource Owner Password โ€” deprecated; never use for new apps.
  • Implicit โ€” deprecated; use Auth Code + PKCE instead.
JWT can't be revoked instantly โ€” it's valid until its exp claim. For "log out everywhere," either keep tokens short-lived (5โ€“15 min) and use a refresh token, or maintain a token-blacklist (giving up half the statelessness).
Always use HTTPS with JWTs. The token is a bearer credential โ€” anyone holding it is the user. Plain HTTP = trivial theft via packet capture.
25

Actuator & Observability โ€” Production-Ready Endpoints, Free

Add spring-boot-starter-actuator and you instantly get:

EndpointReturns
/actuator/healthUP/DOWN, with sub-checks (DB, Redis, disk)
/actuator/infoApp version, git commit, build time
/actuator/metricsJVM, HTTP, DB pool, GC counters
/actuator/prometheusPrometheus-formatted metrics (with Micrometer)
/actuator/loggersRead/change log levels at runtime
/actuator/envAll properties + their source
/actuator/threaddumpJVM thread dump
/actuator/heapdumpHeap dump (for memory leaks)
/actuator/mappingsAll registered URL mappings
Sensible exposure config
management:
  endpoints:
    web:
      exposure:
        include: "health,info,metrics,prometheus"
  endpoint:
    health:
      show-details: when_authorized
  metrics:
    tags:
      application: ${spring.application.name}
      environment: ${spring.profiles.active}

Custom HealthIndicator

Add your own check
@Component
class PaymentGatewayHealth implements HealthIndicator {

    @Override
    public Health health() {
        if (gateway.ping()) return Health.up().build();
        return Health.down().withDetail("reason", "timeout").build();
    }
}

Liveness vs Readiness (Kubernetes)

  • /actuator/health/liveness โ€” "is the JVM alive?" K8s restarts the pod if this fails.
  • /actuator/health/readiness โ€” "ready to serve traffic?" K8s removes the pod from load-balancer rotation if this fails.
Don't expose /actuator/env publicly โ€” it leaks your DB URL, secrets in property values, and feature flags. Default to health, info, metrics, prometheus only; lock the rest behind a private network or auth.
26

Testing Strategy โ€” Unit, Slice, Integration

When would you use @SpringBootTest vs @WebMvcTest vs a plain JUnit test?

The pyramid

TypeAnnotationWhat loadsSpeed
Plain unitNone / @ExtendWith(MockitoExtension.class)Nothing โ€” just your class + mocksFast (ms)
Web slice@WebMvcTest(OrderController.class)Controllers + filters, no service / DB~1s
Data slice@DataJpaTestJPA + in-memory DB, no controllers~1s
Full integration@SpringBootTestFull app context5โ€“20s
A web-slice test
@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired MockMvc mvc;
    @MockBean OrderService svc;

    @Test
    void getOrderReturnsJson() throws Exception {
        when(svc.findById("o-1")).thenReturn(new OrderDto("o-1", 499.0));

        mvc.perform(get("/api/v1/orders/o-1"))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$.id").value("o-1"));
    }
}

Testcontainers โ€” the "real" integration test

Don't test against H2 if production runs Postgres โ€” Hibernate's SQL diverges between dialects. Spin up a real Postgres in a container:

Real DB, on every CI run
@SpringBootTest
@Testcontainers
class OrderIntegrationTest {

    @Container
    static PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:16");

    @DynamicPropertySource
    static void props(DynamicPropertyRegistry r) {
        r.add("spring.datasource.url", pg::getJdbcUrl);
        r.add("spring.datasource.username", pg::getUsername);
        r.add("spring.datasource.password", pg::getPassword);
    }
    // ... tests
}
@SpringBootTest's context is cached across tests with the same config โ€” fast on the second run. Mixing too many configurations forces re-creation and slows the suite massively.
27

Microservices & Spring Cloud โ€” Service Discovery, Config, Gateway

The building blocks

ConcernSpring Cloud answer
Service discoveryEureka (Netflix), Consul, or K8s-native DNS
Config managementSpring Cloud Config Server
Client-side load balancingSpring Cloud LoadBalancer (replaces Ribbon)
Synchronous commsOpenFeign declarative HTTP client
API GatewaySpring Cloud Gateway (reactive)
Distributed tracingMicrometer Tracing + OpenTelemetry / Zipkin
ResilienceResilience4j (retry, circuit breaker, bulkhead)
Async messagingSpring Cloud Stream over Kafka / RabbitMQ

OpenFeign โ€” calling services without writing HTTP plumbing

Declarative HTTP client
@FeignClient(name = "inventory-service")
interface InventoryClient {
    @GetMapping("/items/{sku}")
    Item getItem(@PathVariable String sku);
}

// Then just:
@Service
class CheckoutService {
    private final InventoryClient inventory;
    // ...
    inventory.getItem("SKU-42");   // Spring generates the HTTP call
}

Spring Cloud Gateway routes

Declarative routing
spring:
  cloud:
    gateway:
      routes:
        - id: orders
          uri: lb://orders-service
          predicates:
            - Path=/api/v1/orders/**
          filters:
            - StripPrefix=2
            - AddRequestHeader=X-Origin, gateway
In Kubernetes, you usually don't need Eureka โ€” K8s already does service discovery via DNS. Adding Eureka there is duplicative complexity. Use Spring Cloud Kubernetes if you want config from ConfigMap/Secret without code changes.
28

Resilience4j โ€” Retry, Circuit Breaker, Bulkhead, Rate Limiter

The four patterns

  • Retry โ€” try again on transient failure (with backoff).
  • Circuit Breaker โ€” stop calling a flaky dependency; fail fast for a cooldown window.
  • Bulkhead โ€” limit concurrent calls so one slow dependency can't exhaust the whole thread pool.
  • Rate Limiter โ€” cap calls per time window.
Circuit breaker + retry
@Service
class PaymentService {

    @CircuitBreaker(name = "razorpay", fallbackMethod = "fallback")
    @Retry(name = "razorpay")
    public PaymentResult charge(Order o) {
        return client.post("/charge", o);
    }

    public PaymentResult fallback(Order o, Throwable t) {
        return PaymentResult.queued(o.id);   // degrade gracefully
    }
}
application.yml tuning
resilience4j:
  circuitbreaker:
    instances:
      razorpay:
        failure-rate-threshold: 50     # open if >50% of last N calls fail
        sliding-window-size: 10
        wait-duration-in-open-state: 30s  # cooldown before half-open
  retry:
    instances:
      razorpay:
        max-attempts: 3
        wait-duration: 200ms
        exponential-backoff-multiplier: 2

Circuit breaker states

  • CLOSED โ€” normal; calls go through.
  • OPEN โ€” failure threshold breached; calls fail-fast without hitting the dependency.
  • HALF_OPEN โ€” after wait duration, allow a few probe calls. If they succeed โ†’ CLOSED. If they fail โ†’ back to OPEN.
A circuit breaker in your house cuts power when too much current flows โ€” better to lose lights for a minute than burn the wiring. Same here: better to fail-fast for 30 seconds than take down the whole app waiting on a dead service.
Always provide a fallback for circuit breakers. Without one, an open circuit just throws โ€” and you've moved the problem one level up. With a fallback, a dependency outage degrades gracefully.
29

Spring Boot 3 & Native Image โ€” Why You'll Care About AOT

The big shifts in Spring Boot 3

  • Java 17 minimum โ€” finally drops Java 8.
  • jakarta.* instead of javax.* โ€” the namespace move that broke half of the Java ecosystem in 2022.
  • Spring Framework 6 as the base.
  • First-class GraalVM native image support via Spring AOT.
  • Observability via Micrometer (replaces Sleuth) โ€” tracing + metrics in one library.
  • HTTP interface clients โ€” declarative HTTP without OpenFeign.

Why native images matter

GraalVM ahead-of-time compiles your Spring Boot app to a native executable. Trade-offs:

MetricJVMNative
Startup time2โ€“10 seconds~50ms
Peak memory200โ€“500 MB50โ€“100 MB
Peak throughputHigher (after JIT warmup)Lower (no JIT)
Build timeSecondsMinutes
Reflection / dynamic proxiesFreeMust be declared at build time

Use native for serverless (cold start matters), CLI tools, low-memory containers. Stay on JVM for long-running, high-throughput services.

Native images break anything reflective by default โ€” Hibernate proxies, Jackson, AOP. Spring's AOT processing handles most of the common cases automatically, but third-party libs may need @RegisterReflectionForBinding or hint files.
30

Tricky Gotchas & Curveballs Interviewers Love

These appear when interviewers want to separate "I followed a tutorial" from "I shipped Spring." Memorize a few; they pay back massively.

1. Self-invocation kills annotations

Calling this.foo() from another method in the same bean bypasses the AOP proxy. @Transactional, @Cacheable, @Async, custom aspects โ€” all silently no-op. Fix: inject self-reference, split into two beans, or use AspectJ load-time weaving.

2. @Value on a static field doesn't work

Spring injects values via setters/fields on instances. Statics are class-level, set before any instance exists. Use a non-static field, or @PostConstruct to copy a non-static into a static.

3. Bean creation order is not source order

Spring resolves the dependency graph and instantiates in topological order. Don't assume "A is declared above B โ†’ A is created first." If you need explicit ordering, use @DependsOn.

4. Circular dependencies

A โ†’ needs B โ†’ needs A. Spring used to allow this with field/setter injection by partial wiring. Spring Boot 2.6+ fails on startup by default. Fix: refactor (the cycle is a design smell), use @Lazy on one side, or set spring.main.allow-circular-references=true (don't).

5. List<MyBean> auto-injection

Spring will inject all beans of type MyBean as a list. Useful for plugin patterns ("apply every OrderValidator"). Combine with @Order for deterministic ordering.

Plugin via list injection
@Service
class OrderProcessor {
    private final List<OrderValidator> validators;   // all impls injected

    public void validate(Order o) {
        validators.forEach(v -> v.validate(o));
    }
}

6. @RequestParam required-true is the default

Forgot to mark it optional? Missing param = 400 Bad Request. Use @RequestParam(required=false) or a default: @RequestParam(defaultValue="0").

7. @Transactional on JPA: changes outside the transaction are NOT flushed

Setting an entity field outside a @Transactional method (or after commit) doesn't persist. The entity becomes "detached." A common WTF moment.

8. save() doesn't always trigger an INSERT

Spring Data's save() returns the merged entity. For a NEW entity (id=null), Hibernate INSERTs. For an EXISTING one, it's a SELECT-then-UPDATE โ€” and if no fields changed, no UPDATE. Don't assume one DB hit per save().

9. @MockBean changes the context

Each unique mock combination = a different ApplicationContext = re-bootstrap on every test class. Slow test suites often trace to this. Standardize @MockBean usage; consider @SpyBean sparingly.

10. @SpringBootApplication position matters

Component scan starts from the package of the @SpringBootApplication class. Put it in com.example.app and it scans com.example.app and below. A bean in com.example.other won't be picked up. Always place the main class at the root package.

11. application.properties and application.yml together

You can have both. Spring loads both, but application.properties takes precedence over application.yml. If a property exists in both, properties wins. Pick one and stick with it.

12. @Bean vs @Component

@Component is on the class โ€” Spring instantiates it via constructor. @Bean is on a method inside @Configuration โ€” you control how the bean is built. Use @Bean when you need to wire third-party classes (you can't put @Component on someone else's library class) or when construction has logic.

13. @RestController + entity return = potential problem

Returning a JPA entity directly from a controller serializes lazy associations (LazyInitializationException) or pulls the world (eager). Return DTOs, not entities. The DTO mapping is one boilerplate step that pays back tenfold.

14. JSON deserialization without default constructor

Jackson needs either a no-arg constructor + setters, or constructor with @JsonCreator. Java records work out of the box (Jackson 2.12+).

15. @RequestBody with a String parameter

Trips up some devs: with content-type application/json, Spring tries to parse the body as a JSON-encoded String. Use consumes = "text/plain" or accept a byte[]/InputStream for raw bodies.

16. Logging defaults โ€” Logback, not Log4j

Spring Boot ships Logback as the default. Bringing in Log4j 2 means excluding spring-boot-starter-logging first, else both load and log lines double.

17. WebClient vs RestTemplate

RestTemplate is in maintenance mode (no new features) but not deprecated. WebClient (from WebFlux) is recommended for new code โ€” it's reactive but works synchronously too. In Spring 6.1+, RestClient is the new sync-friendly alternative โ€” WebClient-style fluent API without the reactive types.

18. Property placeholder fallbacks

${db.url:jdbc:h2:mem:default} โ€” colon = default if missing. Saves countless "missing property" startup failures in dev.

19. spring.jpa.open-in-view defaults to TRUE

This is the controversial one. It keeps the Hibernate session open through the entire request โ€” so lazy associations work in views/controllers. But it also: hides N+1 (now they happen during rendering), holds DB connections longer, and masks design issues. Most teams turn it off (false) and load eagerly via DTOs / EntityGraphs.

20. @Configuration vs @Configuration(proxyBeanMethods = false)

By default, @Configuration classes are CGLIB-proxied so calls between @Bean methods return the singleton, not a new instance. Setting proxyBeanMethods = false skips the proxy โ†’ faster startup, smaller native images, but you lose that singleton-via-method-call magic. Spring Boot 2.2+ uses this in its own auto-configurations.

If you can pull off five of these in an interview, you've moved from "I read the docs" to "I've debugged this in production at 2 AM." Interviewers can tell the difference instantly.
When you don't know an answer, don't bluff. Say "I haven't hit that, but I'd guess..." and reason from first principles. Interviewers value transparent thinking far more than perfect recall.

Did this guide make Spring Boot click? If it helped, tap the โค๏ธ โ€” that's how I know it landed.