The 60 Spring Boot questions interviewers actually ask, with direct answers, runnable code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
Spring Boot is an opinionated Java framework layered on top of the Spring Framework. It exists to cut the setup work that plain Spring used to demand: XML config, manual bean wiring, and a separate application server. According to the official Spring Boot reference documentation, it lets you build stand-alone, production-grade applications that you can just run, and it takes an opinionated view of the Spring platform so you get started with minimum fuss. The two ideas that carry the whole framework are auto-configuration, which wires beans based on the jars on your classpath, and starter dependencies, which bundle the libraries a feature needs into one import. An embedded server ships inside the jar, so deployment is java -jar app.jar. In interviews, Spring Boot questions probe how these pieces connect: dependency injection and the IoC container, how auto-configuration decides what to create, the REST and data layers, and the production concerns (actuator, profiles, exception handling) that come up on real teams. This page collects the 60 questions that come up most, each with a direct answer and runnable code. Increasingly the first backend round is a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: Spring Boot Tutorial for Beginners (Java Framework)
Video: Spring Boot Tutorial for Beginners (Java Framework) (freeCodeCamp.org, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your Spring Boot certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
Spring Boot is a Java framework built on the Spring Framework that removes most setup work. It auto-configures beans based on the libraries on your classpath, bundles dependencies through starters, and embeds a server so an app runs as a plain java -jar.
Teams use it because it cuts the boilerplate that plain Spring demanded: no XML wiring, no separate server to install, and sensible defaults you can override. You go from empty project to a running REST endpoint in minutes instead of hours.
Key point: A one-line definition plus the two ideas that carry it (auto-configuration and starters) beats a memorized feature list. Interviewers open with this to hear how you organize an answer.
Watch a deeper explanation
Video: Spring Boot Quick Start 1 - Introduction (Java Brains, YouTube)
Spring is the core framework: the IoC container, dependency injection, AOP, and modules for web, data, and security. Spring Boot sits on top and configures Spring for you, adding auto-configuration, starter dependencies, an embedded server, and production tooling like actuator.
Put simply, Spring Boot doesn't replace Spring; it's Spring with the wiring and server setup done automatically. Every dependency injection question is a Spring question even in a Boot interview.
| Spring Framework | Spring Boot | |
|---|---|---|
| Configuration | Manual (Java or XML) | Auto-configured from classpath |
| Server | Bring your own | Embedded (Tomcat by default) |
| Dependencies | Chosen one by one | Bundled via starters |
| Setup effort | Higher | Lower, opinionated defaults |
Key point: The follow-up is usually 'so what does Boot actually generate?'. Have auto-configuration and the embedded server ready.
@SpringBootApplication is a meta-annotation that combines three: @Configuration (the class can define beans), @EnableAutoConfiguration (turn on auto-configuration), and @ComponentScan (scan this package and below for components).
You put it on the main class, and its single main() method calls SpringApplication.run to start the whole application. One annotation bootstraps everything, which is why every Spring Boot app has exactly one of these.
@SpringBootApplication
public class ShopApplication {
public static void main(String[] args) {
SpringApplication.run(ShopApplication.class, args);
}
}Key point: Naming the three annotations it bundles is the depth marker. Just saying 'it starts the app' indicates surface knowledge.
Inversion of control means the framework, not your code, controls object creation and wiring. Dependency injection is how it does that: instead of a class building its own collaborators with new, the container creates them and supplies them.
The payoff is looser coupling and easier testing. A service that receives a repository through its constructor can be tested with a mock repository, with no container involved. Spring's IoC container reads your beans and injects each one's dependencies.
@Service
public class OrderService {
private final OrderRepository repo;
// container injects the repository here
public OrderService(OrderRepository repo) {
this.repo = repo;
}
}Key point: Interviewers set this up to lead into 'which injection type do you prefer?'. Answer crisply and expect that follow-up.
Watch a deeper explanation
Video: Spring Tutorial 01 - Understanding Dependency Injection (Java Brains, YouTube)
Three types: constructor injection (dependencies passed to the constructor), setter injection (through setter methods), and field injection (@Autowired straight on a field). Constructor injection is the recommended default.
Constructor injection makes dependencies explicit and required, allows final fields, and lets you build the object in a test without Spring. Field injection hides dependencies and needs reflection to set in tests, which is why teams move away from it.
// preferred: constructor injection, dependency is final and explicit
@Service
public class PayService {
private final Gateway gateway;
public PayService(Gateway gateway) {
this.gateway = gateway;
}
}| Type | How | Trade-off |
|---|---|---|
| Constructor | Passed to constructor | Explicit, testable, final fields (preferred) |
| Setter | Through setters | Good for optional dependencies |
| Field | @Autowired on field | Concise but hides deps, hard to test |
Key point: Saying 'constructor injection' and explaining why (final fields, testability) is the answer interviewers hope to hear.
A bean is any object that the Spring IoC container creates, wires, and manages. You mark a class with a stereotype like @Component or declare it with @Bean in a configuration class, and the container handles its lifecycle and dependencies.
The key point is management: beans aren't just objects you make with new. Spring controls when they're created, what gets injected into them, and when they're destroyed.
Key point: The natural follow-up is 'how do you define one?'. Have both @Component scanning and @Bean methods ready.
They're all stereotype annotations that register a class as a bean, so functionally they overlap. The difference is intent and a little extra behavior. @Component is the generic marker. @Service marks business logic. @Controller marks a web controller. @Repository marks a data access class.
@Repository adds real behavior: it translates database-specific exceptions into Spring's DataAccessException hierarchy. The others are mostly semantic labels that make the code's layers obvious.
| Annotation | Layer | Extra behavior |
|---|---|---|
| @Component | Generic | None, base stereotype |
| @Service | Business logic | None, semantic only |
| @Repository | Data access | Exception translation to DataAccessException |
| @Controller | Web | Handles web requests (with @RequestMapping) |
Key point: @Repository does exception translation while the others are semantic is the detail that.
@Autowired tells Spring to inject a matching bean at that point: a constructor, a setter, or a field. Spring finds a bean of the required type in the container and wires it in automatically.
On a class with a single constructor, @Autowired is optional now; Spring injects through it anyway. When more than one bean matches a type, you disambiguate with @Qualifier or @Primary.
@Service
public class Notifier {
private final MailSender sender;
// @Autowired optional on a single constructor
public Notifier(MailSender sender) {
this.sender = sender;
}
}A starter is a dependency descriptor that pulls in a curated, version-compatible set of libraries for a feature. Add spring-boot-starter-web and you get Spring MVC, Jackson for JSON, validation, and embedded Tomcat in one line.
Starters solve dependency hell: you stop hand-picking library versions that work together, and Boot's dependency management keeps them aligned. Common ones are starter-web, starter-data-jpa, starter-security, and starter-test.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>Auto-configuration wires beans automatically based on what's on the classpath, what beans already exist, and which properties are set. Add the JPA starter and a database driver, and Boot configures a DataSource, an EntityManager, and a transaction manager without you writing that code.
It works through conditional configuration classes marked with annotations like @ConditionalOnClass and @ConditionalOnMissingBean, so a bean is only created when it's needed and you haven't defined your own.
Key point: The killer follow-up is 'how does it decide?'. Answering with @Conditional shows you understand the mechanism, not just the outcome.
@Controller is the general web controller; its methods usually return a view name that a template engine renders into HTML. @RestController is @Controller combined with @ResponseBody, so method return values are serialized directly into the response body, typically JSON.
Use @RestController for REST APIs and @Controller when you're returning server-rendered pages. With @RestController you never write @ResponseBody on each method because it's already applied.
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.find(id); // serialized to JSON
}
}@RequestMapping maps a URL path (and optionally an HTTP method) to a handler method or a whole controller. @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, and @PatchMapping are shortcuts for @RequestMapping with the method already fixed.
You typically put a base path with @RequestMapping on the controller and specific verbs on each method. This keeps route definitions readable and matches REST conventions.
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping // GET /api/orders
List<Order> list() { ... }
@PostMapping // POST /api/orders
Order create(@RequestBody Order o) { ... }
}@PathVariable pulls a value out of the URL path itself, like the id in /users/42. @RequestParam pulls a value from the query string or form data, like page in /users?page=2.
Rule of thumb: path variables identify a resource, query parameters filter, sort, or paginate it. @RequestParam can also be marked optional with a default value, which is common for pagination.
@GetMapping("/users/{id}")
User byId(@PathVariable Long id,
@RequestParam(defaultValue = "false") boolean full) {
// GET /users/42?full=true
}@RequestBody binds the incoming HTTP request body (usually JSON) to a method parameter, deserializing it into a Java object with a message converter like Jackson. @ResponseBody does the reverse: it serializes the return value into the response body instead of treating it as a view name.
In a @RestController, @ResponseBody is applied for you, so you mostly write @RequestBody explicitly on the parameter you want bound from the body.
@PostMapping("/api/users")
public User create(@RequestBody User incoming) {
// JSON body -> User object automatically
return userService.save(incoming);
}It's the central configuration file for a Spring Boot app. You set server port, database URL and credentials, logging levels, and any custom properties there, and Boot reads them at startup.
You can use .properties or the .yml equivalent; they do the same thing with different syntax. Values can be overridden by environment variables and command-line arguments, which is how the same jar runs differently in each environment.
server.port=8081
spring.datasource.url=jdbc:postgresql://localhost:5432/shop
spring.jpa.hibernate.ddl-auto=validate
logging.level.org.springframework=INFOAn embedded server is a servlet container packaged inside the application jar rather than installed separately. Spring Boot's web starter embeds Tomcat by default, so the app starts its own server when you run java -jar.
This is why deployment is one command with no external Tomcat to configure. You can swap Tomcat for Jetty or Undertow by excluding the default and adding the other starter.
Key point: The follow-up is 'how would you switch to Jetty?'. Knowing you exclude the Tomcat starter and add the Jetty one shows hands-on experience.
Spring Initializr is a project generator at start.spring.io (also built into most IDEs) that scaffolds a new Spring Boot project. You pick the build tool, language, Boot version, and the starters you need, and it produces a ready-to-run zip.
It saves you from hand-writing the build file and picking compatible dependency versions. Most teams start every new service this way.
Three common ways: run the main class from your IDE, use the build plugin (mvn spring-boot:run or gradle bootRun) during development, or build the jar and run java -jar target/app.jar for anything production-like.
The build produces a single executable fat jar with the embedded server and all dependencies inside. That one file is the whole deployable, which is what makes Boot easy to containerize.
# during development
mvn spring-boot:run
# build then run the fat jar
mvn clean package
java -jar target/shop-0.0.1-SNAPSHOT.jarWatch a deeper explanation
Video: Spring Boot Tutorials | Full Course (Telusko, YouTube)
The bootstrap one (@SpringBootApplication), the stereotypes (@Component, @Service, @Repository, @Controller, @RestController), injection (@Autowired, @Qualifier), web mapping (@RequestMapping and the @GetMapping family, @PathVariable, @RequestParam, @RequestBody), config (@Configuration, @Bean, @Value), and data (@Entity, @Transactional).
You don't recite the list in an interview; you recognize each one and explain what it does when it comes up. Grouping them by role (bootstrap, stereotype, web, config, data) is a clean way to talk about them.
@Component goes on your own class so component scanning picks it up automatically. @Bean goes on a method inside a @Configuration class, and the method's return value becomes a bean the container manages.
Use @Component for classes you write and control. Use @Bean when you need to register an object you can't annotate, like a class from a third-party library, or when creating the bean needs custom logic.
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
// registering a library class you can't annotate
return new RestTemplate();
}
}@Value injects a single value from configuration into a field or parameter, using property placeholder syntax. You reference a property from application.properties, an environment variable, or a literal default.
It's fine for a few loose values. When a class needs many related properties, @ConfigurationProperties binds them into a typed object, which is cleaner and validated. Interviewers like to hear you know when to switch.
@Component
public class Mailer {
@Value("${mail.from:noreply@shop.com}")
private String fromAddress; // uses default if property missing
}Spring MVC is the web framework inside the Spring Framework. It handles the request lifecycle: a front controller (DispatcherServlet) routes each request to a handler method, binds parameters, and renders the response.
Spring Boot doesn't replace MVC; it auto-configures it. Adding spring-boot-starter-web brings in Spring MVC and sets up the DispatcherServlet, message converters, and the embedded server so your controllers just work.
A JPA repository is an interface you extend (usually JpaRepository) to get database operations without writing implementation code. Spring Data generates the implementation at runtime, giving you save, findById, findAll, delete, and paging for free.
You add custom queries just by naming a method, like findByEmail, and Spring derives the query from the name. For anything complex you write @Query with JPQL or native SQL.
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email); // query derived from name
List<User> findByActiveTrue();
}Key point: Showing you know query-derivation-from-method-names, and when to fall back to @Query, is exactly the Spring Data fluency this checks.
@Entity marks a class as a JPA entity, so Hibernate maps it to a database table. @Id marks the primary key, @GeneratedValue tells the database to generate it, and @Column customizes a field's column name or constraints. Without extra config, the table and column names come from the class and field names.
You add @Table to override the table name and mapping annotations like @OneToMany or @ManyToOne to model relationships. An entity is a plain class with these annotations, not a special base type.
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
}For candidates with working experience: bean lifecycle, transactions, exception handling, and the questions that separate users from understanders.
A scope controls how many instances of a bean the container creates and how long each lives. The default is singleton: one shared instance per container. prototype creates a new instance on every request for the bean.
Web scopes add request (one per HTTP request) and session (one per user session). Most beans should stay singleton and be stateless; reach for other scopes only when a bean genuinely needs per-request or per-session state.
| Scope | Instances | Typical use |
|---|---|---|
| singleton (default) | One per container | Stateless services, repositories |
| prototype | New on each injection/lookup | Stateful, short-lived objects |
| request | One per HTTP request | Per-request web state |
| session | One per HTTP session | Per-user web state |
Key point: The trap is a stateful singleton. Saying singletons should be stateless because they're shared across threads is the safety awareness the question needs.
The container instantiates the bean, injects its dependencies, runs any aware-interface callbacks, then post-processors run before and after initialization, and initialization callbacks fire (@PostConstruct or an init method). The bean is now ready. On shutdown, destruction callbacks run (@PreDestroy or a destroy method).
You rarely need most of it, but @PostConstruct for setup after injection and @PreDestroy for cleanup are worth knowing by name. BeanPostProcessors are how features like AOP hook in.
@Component
public class CacheWarmer {
@PostConstruct
public void init() {
// runs once after dependencies are injected
preload();
}
@PreDestroy
public void cleanup() {
flush();
}
}@Transactional wraps a method in a database transaction using a proxy: it opens a transaction before the method, commits on normal return, and rolls back on a runtime exception by default. Checked exceptions don't trigger rollback unless you configure them.
The classic pitfalls come from the proxy. Calling a @Transactional method from another method in the same class bypasses the proxy, so no transaction starts. And by default only unchecked exceptions roll back, which surprises people who throw a checked exception expecting a rollback.
@Service
public class TransferService {
@Transactional
public void transfer(Long from, Long to, BigDecimal amount) {
debit(from, amount);
credit(to, amount); // if this throws a RuntimeException, both roll back
}
}Key point: Mentioning self-invocation bypassing the proxy is the detail that marks real production experience with Spring transactions.
Propagation decides what happens when a transactional method is called while another transaction is already running. REQUIRED (the default) joins the existing transaction or starts one if none exists. REQUIRES_NEW suspends the current one and runs in a fresh, independent transaction.
REQUIRES_NEW matters when an inner operation must commit or roll back on its own, like writing an audit log that should persist even if the outer transaction fails. Other modes (SUPPORTS, MANDATORY, NESTED) come up less but are worth recognizing.
| Propagation | If a transaction exists | If none exists |
|---|---|---|
| REQUIRED (default) | Join it | Start a new one |
| REQUIRES_NEW | Suspend it, start a new one | Start a new one |
| SUPPORTS | Join it | Run non-transactionally |
| MANDATORY | Join it | Throw an exception |
The clean approach is @RestControllerAdvice with @ExceptionHandler methods. You catch exception types in one place and map each to a proper HTTP status and a consistent error body, instead of try/catch scattered across controllers.
For per-controller handling you can put @ExceptionHandler on the controller itself, and @ResponseStatus on a custom exception sets its status. Centralizing it with @RestControllerAdvice keeps error responses uniform across the whole API.
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiError> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ApiError(ex.getMessage()));
}
}Key point: Reaching for @RestControllerAdvice over try/catch in every controller is the answer that indicates maintainable-code instinct.
ResponseEntity represents the full HTTP response: status code, headers, and body. Returning it gives you explicit control instead of relying on defaults, so you can send 201 Created with a Location header or 404 with an error body.
For simple success cases, returning the object directly is fine because Boot defaults to 200. Use ResponseEntity when the status or headers matter, which is most non-trivial endpoints.
@PostMapping("/api/users")
public ResponseEntity<User> create(@RequestBody User u) {
User saved = service.save(u);
return ResponseEntity
.created(URI.create("/api/users/" + saved.getId()))
.body(saved); // 201 with Location header
}Add Bean Validation annotations (@NotNull, @Size, @Email, @Min) to the fields of your request object, then put @Valid on the controller parameter. Spring validates before your method runs and throws MethodArgumentNotValidException on failure.
You catch that exception in a @RestControllerAdvice to return a clean 400 with the field errors. The validation starter (or spring-boot-starter-validation) brings in the implementation.
public record SignupRequest(
@NotBlank String name,
@Email String email,
@Size(min = 8) String password) {}
@PostMapping("/signup")
public User signup(@Valid @RequestBody SignupRequest req) { ... }Profiles let you group configuration and beans per environment: dev, test, prod. You put environment-specific properties in application-dev.properties and mark environment-specific beans with @Profile, then activate a profile at startup.
The same jar then behaves differently per environment based on which profile is active, set through spring.profiles.active as a property, environment variable, or command-line flag. This keeps one build promoting cleanly from dev to prod.
@Bean
@Profile("prod")
public DataSource prodDataSource() { ... }
// activate: --spring.profiles.active=prod
// or env: SPRING_PROFILES_ACTIVE=prodActuator adds production-ready endpoints for monitoring and managing a running app. Out of the box it exposes /actuator/health (is the app up, are dependencies reachable), /actuator/metrics, /actuator/info, and more, once you add the starter.
By default only health is exposed over HTTP; you opt others in through configuration, and you secure them because some reveal internals. Health checks feed load balancers and Kubernetes readiness/liveness probes.
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=when-authorized@Conditional annotations register a bean only when a condition holds. @ConditionalOnClass checks a class is on the classpath, @ConditionalOnMissingBean creates a bean only if you haven't defined your own, and @ConditionalOnProperty gates on a property value.
This is the engine behind auto-configuration. Boot's auto-config classes are covered in these conditions, so a DataSource is configured only when a driver is present and you haven't declared one yourself. That's how Boot backs off the moment you take control.
@Configuration
public class CacheConfig {
@Bean
@ConditionalOnMissingBean // only if the user hasn't defined one
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
}Key point: Connecting @ConditionalOnMissingBean to 'this is why my custom bean overrides Boot's default' shows you understand the whole auto-config model.
Two tools: @Primary marks one bean as the default choice when several match, and @Qualifier names a specific bean at the injection point. @Qualifier wins over @Primary because it's explicit.
Without either, Spring throws a NoUniqueBeanDefinitionException at startup because the wiring is ambiguous. This comes up whenever you have two implementations of an interface, like two payment gateways.
@Service
public class Checkout {
private final PaymentGateway gateway;
public Checkout(@Qualifier("stripeGateway") PaymentGateway gateway) {
this.gateway = gateway;
}
}An entity maps to a database table and carries persistence concerns. A DTO shapes what your API sends and receives. Keeping them separate means your database schema and your API contract can change independently, and you don't leak internal fields or lazy-loaded relationships into JSON.
Returning entities straight from controllers is a common early mistake: it couples the API to the schema, risks serializing lazy associations (triggering extra queries or exceptions), and can expose fields you never meant to send.
Key point: Naming the lazy-loading serialization trap, not just 'separation of concerns', is what makes this answer land as experience.
Fetch type decides when a related entity is loaded. EAGER loads the association immediately with the parent. LAZY defers loading until the association is actually accessed. Collections default to LAZY, single associations to EAGER.
LAZY is usually right because it avoids loading data you don't need, but it causes LazyInitializationException if you access the association after the persistence context closes. The N+1 query problem also hides here: lazily loading a collection inside a loop fires one query per item.
Key point: Bringing up the N+1 problem in practice is a strong signal. The fix (a fetch join or an entity graph) is the expected follow-up.
N+1 happens when you load N parent rows in one query, then trigger one extra query per parent to load a lazy association, so N+1 queries total. It quietly kills performance as data grows: a page listing 100 orders can fire 101 queries.
Fixes: a JPQL fetch join (join fetch) to load parents and children in one query, an @EntityGraph on the repository method, or batch fetching. The point in an interview is to recognize it from the symptom (many similar queries in the logs) and name a fix.
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("select o from Order o join fetch o.items")
List<Order> findAllWithItems(); // one query, no N+1
}Spring Security inserts a chain of servlet filters in front of your app. A request passes through filters that handle authentication (who are you) and authorization (are you allowed). You configure the chain with a SecurityFilterChain bean that declares which URLs are public, which need a role, and how login works.
For APIs it's commonly stateless with JWT or OAuth2 tokens instead of sessions. The mental model to convey is filter chain, then authentication, then authorization, then your controller.
@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(a -> a
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()));
return http.build();
}How a request moves through Spring Security
For stateless APIs the authentication step reads a JWT or OAuth2 token instead of a session.
Watch a deeper explanation
Video: Spring Boot Tutorial | Full Course [2023] [NEW] (Amigoscode, YouTube)
CORS (cross-origin resource sharing) is a browser rule that blocks a page on one origin from calling an API on another origin unless the API opts in with the right response headers. It bites when a frontend on one domain calls a Spring Boot API on another.
You configure it with @CrossOrigin on a controller for narrow cases, or globally with a CorsConfigurationSource or WebMvcConfigurer. In a secured app, CORS config lives in the security filter chain so it applies before authorization.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST");
}
}Two levels. Unit tests exercise a single class with mocked dependencies (Mockito), no Spring context, which keeps them fast. Slice and integration tests load part or all of the context: @WebMvcTest for the web layer with MockMvc, @DataJpaTest for repositories, and @SpringBootTest for a full context.
The judgment the question needs is using the narrowest slice that proves what you need. @SpringBootTest for everything is slow; @WebMvcTest to test a controller without touching the database is the discipline.
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mvc;
@MockBean UserService service;
@Test
void returnsUser() throws Exception {
when(service.find(1L)).thenReturn(new User(1L, "Asha"));
mvc.perform(get("/api/users/1"))
.andExpect(status().isOk());
}
}AOP lets you apply cross-cutting behavior (logging, timing, security checks, transactions) across many methods without editing each one. You write an aspect with advice (the code to run) and a pointcut (which methods it applies to), and Spring weaves it in via proxies.
It's not just a theory question: @Transactional itself is implemented with AOP. Understanding that proxies power AOP explains why self-invocation bypasses @Transactional, which ties two topics together.
@Aspect
@Component
public class TimingAspect {
@Around("@annotation(Timed)")
public Object time(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try { return pjp.proceed(); }
finally { log.info("{} ns", System.nanoTime() - start); }
}
}@ConfigurationProperties binds a group of related properties, matched by a prefix, into the fields of a typed class. Instead of scattering many @Value annotations, you get one object with all the settings, type-safe and easy to validate.
Use @Value for one or two loose values; use @ConfigurationProperties once a feature has several related settings. It also supports nested objects, lists, and JSR-303 validation on the bound class.
@ConfigurationProperties(prefix = "mail")
public class MailProperties {
private String from;
private int retries;
// getters/setters bound from mail.from, mail.retries
}advanced rounds probe internals, design judgment, and production scars. Expect every answer here to draw a follow-up.
@EnableAutoConfiguration triggers an import that reads auto-configuration class names from a metadata file on the classpath (META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports in Boot 3). Spring evaluates each class's @Conditional annotations and applies only those whose conditions pass.
Ordering matters, so classes declare @AutoConfigureBefore/After, and @ConditionalOnMissingBean makes user-defined beans win. You can see what fired by running with the debug flag, which prints a positive and negative match report. Naming that report is a strong production signal.
Key point: Mentioning the AutoConfiguration.imports file and the conditions report (--debug) proves you've actually looked inside, not just read about it.
Watch a deeper explanation
Video: The ULTIMATE Guide to Spring Boot: Spring Boot for Beginners (Devtiro, YouTube)
A custom starter is an auto-configuration module plus a thin starter POM. You write a configuration class with @Conditional guards and @ConfigurationProperties, register it in the AutoConfiguration.imports metadata file, and package it so any app that adds the dependency gets the feature auto-wired.
The reason to build one is reuse across many services: shared security config, a company logging setup, a client for an internal service. Follow the naming convention (yourname-spring-boot-starter) and use @ConditionalOnMissingBean so consumers can override your defaults.
Enable it with server.shutdown=graceful. On a shutdown signal, the server stops accepting new requests but lets in-flight requests finish within a timeout before the context closes. This prevents dropping requests mid-flight during a deploy or a pod rollover.
Pair it with @PreDestroy hooks for cleanup and, in Kubernetes, a preStop hook and readiness probe so traffic drains before the process exits. Getting this right is what stops rolling deploys from throwing errors at users.
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30sTwo broad styles. Synchronous request/response over HTTP (a REST client like RestClient, WebClient, or a declarative Feign-style client), which is simple and immediate but couples services in time: if the callee is down, the caller waits or fails. Asynchronous messaging over a broker (Kafka, RabbitMQ), which decouples services and absorbs load but adds eventual consistency and operational complexity.
The production-ready answer is picking per use case: synchronous for a read you need right now, messaging for events and work that can happen slightly later. And you add resilience (timeouts, retries, circuit breakers) to any synchronous call so one slow service doesn't cascade.
| Style | Good for | Cost |
|---|---|---|
| Synchronous REST | Immediate reads, simple flows | Temporal coupling, cascading failures |
| Async messaging | Events, decoupling, load spikes | Eventual consistency, broker to operate |
A circuit breaker stops calling a failing dependency so one bad service doesn't drag yours down. After a threshold of failures it opens and fails fast (or serves a fallback) instead of piling up slow calls, then periodically tries again to see if the dependency recovered.
In Spring Boot you add Resilience4j and annotate the call with @CircuitBreaker plus a fallback method, often alongside @Retry and @RateLimiter. The state machine (closed, open, half-open) is the concept to explain; the annotation is just how you apply it.
@CircuitBreaker(name = "inventory", fallbackMethod = "stockUnknown")
public Stock getStock(String sku) {
return inventoryClient.fetch(sku);
}
public Stock stockUnknown(String sku, Throwable t) {
return Stock.unknown(sku); // fallback when the circuit is open
}You don't use a single database transaction across services, because each owns its own data and there's no shared connection. The common pattern is the saga: break the operation into local transactions per service, coordinated by events, with a compensating action to undo each step if a later step fails.
Sagas come in two flavors: choreography (services react to each other's events) and orchestration (a central coordinator drives the steps). The honest framing is that you trade atomicity for availability, so you design for eventual consistency and idempotent handlers instead of pretending distributed ACID is free.
Key point: Saying 'no distributed ACID, use a saga with compensating actions' is the answer. Candidates who reach for two-phase commit usually haven't run it in production.
Externalize config so the same image runs anywhere: environment variables and mounted config for deployment-specific values, and a central config source (Spring Cloud Config, Consul, or a Kubernetes ConfigMap) when many services share settings. Bind it into typed @ConfigurationProperties and validate at startup so a bad value fails fast.
Secrets never live in the repo or the image. They come from a secret manager (Vault, a cloud secrets service, or Kubernetes Secrets) injected at runtime. The anti-patterns to name: hard-coded credentials, .env files baked into images, and config read ad hoc from os-level lookups deep in the call stack.
WebFlux is Spring's reactive, non-blocking web stack built on Project Reactor (Mono and Flux) and an event loop, versus Spring MVC's thread-per-request blocking model. WebFlux can serve many concurrent connections with few threads because it doesn't block a thread while waiting on I/O.
Choose WebFlux for high-concurrency, I/O-bound workloads (many slow downstream calls, streaming, lots of idle connections), and only if your whole stack is non-blocking, because one blocking call stalls an event-loop thread. For typical CRUD services, MVC is simpler and easier to debug, so WebFlux is a deliberate choice, not a default.
| Spring MVC | Spring WebFlux | |
|---|---|---|
| Model | Thread per request, blocking | Event loop, non-blocking |
| Best at | Standard CRUD, simplicity | High concurrency, streaming I/O |
| Return types | Objects, ResponseEntity | Mono, Flux |
| Risk | Threads tied up on slow I/O | One blocking call stalls the loop |
Spring's cache abstraction sits above a provider (Caffeine, Redis, and others). You enable it with @EnableCaching, then annotate methods: @Cacheable stores and reuses a result keyed by arguments, @CacheEvict removes entries, @CachePut updates them. The abstraction is provider-agnostic, so switching from an in-memory cache to Redis is mostly config.
The judgment part is what to cache and how to invalidate: cache expensive, read-heavy, rarely-changing data, set a TTL, and think through stale reads. In a distributed system a local cache per instance drifts, so a shared store like Redis keeps instances consistent.
@Service
public class RateService {
@Cacheable(value = "rates", key = "#currency")
public BigDecimal current(String currency) {
return remoteRateApi.fetch(currency); // called only on cache miss
}
}@Async runs a method on a separate thread from a task executor, so the caller doesn't block; the method should return void or a CompletableFuture. @Scheduled runs a method on a timer, by fixed rate, fixed delay, or a cron expression. Both need @EnableAsync or @EnableScheduling to switch the feature on.
The gotchas mirror @Transactional because both use proxies. Calling an @Async method from within the same class bypasses the proxy and runs synchronously. And the default executor is a simple single-thread one, so for real load you define your own ThreadPoolTaskExecutor with a bounded queue rather than letting tasks pile up.
@Service
public class ReportService {
@Async
public CompletableFuture<Report> build(Long id) {
return CompletableFuture.completedFuture(generate(id));
}
@Scheduled(cron = "0 0 2 * * *") // 2am daily
public void nightlyCleanup() { purgeOldReports(); }
}Key point: Naming the self-invocation trap and the need for a custom executor is what separates 'I've used @Async' from 'I've run it in production'.
Spring Boot uses HikariCP by default. The setting that matters most is maximum-pool-size, and bigger is not better. A pool larger than the database can handle just moves contention from your app into the database and adds latency. Right-sizing follows the load the database can actually serve concurrently, often a small number.
You also tune connection-timeout (how long a thread waits for a connection before failing) and max-lifetime (recycle connections before the database or a proxy drops them). Under-sizing shows up as threads blocked waiting for a connection; over-sizing shows up as database overload. Measuring pool metrics from actuator is how you find the right number.
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.connection-timeout=3000
spring.datasource.hikari.max-lifetime=1800000First, trim what loads: fewer starters, lazy bean initialization (spring.main.lazy-initialization) for dev, and excluding auto-configurations you don't use. Profile startup with the ApplicationStartup metrics to find slow beans. These help but keep the normal JVM.
For a step change, GraalVM native images (via Spring's AOT support and the native build) compile ahead of time to a native executable that starts in tens of milliseconds with much lower memory, at the cost of longer builds and reflection constraints. That trade-off is exactly what serverless and fast-scaling deployments want.
Three pillars: logs (structured JSON with a correlation/trace id on every line), metrics (Micrometer exposing to Prometheus through actuator), and traces (Micrometer Tracing propagating a trace id across service calls so you can follow one request end to end). Actuator health endpoints feed the orchestrator's probes.
The senior point is correlation. A trace id that flows through logs, metrics, and traces is what lets you go from a spike on a dashboard to the exact request and log line. Wiring all three to a backend (Prometheus, Grafana, an OpenTelemetry collector) is the deliverable, not just turning on actuator.
Idempotent means retrying the same request has the same effect as making it once, which matters because networks drop responses and clients retry. GET and PUT are naturally idempotent; POST that creates a resource is not, so a retry can double-charge or duplicate.
The standard technique is an idempotency key: the client sends a unique key per logical operation, and the server records processed keys (in a database or Redis with a TTL). A repeated key returns the stored result instead of doing the work again. Combined with unique constraints at the database, this makes retries safe.
@PostMapping("/payments")
public ResponseEntity<Payment> pay(
@RequestHeader("Idempotency-Key") String key,
@RequestBody PaymentRequest req) {
return store.find(key)
.map(existing -> ResponseEntity.ok(existing))
.orElseGet(() -> ResponseEntity.ok(process(key, req)));
}The headline change is the move from the javax namespace to jakarta for the Jakarta EE APIs (javax.persistence becomes jakarta.persistence), which forces dependency and import updates when you migrate. Spring Boot 3 also raised the baseline to Java 17 and Spring Framework 6.
Other notable additions: first-class GraalVM native image support through AOT processing, observability built on Micrometer (including tracing), and Problem Details (RFC 7807) for error responses. Knowing the jakarta migration pain is the answer that indicates having actually done an upgrade.
Observe first: check actuator metrics and traces to see where time goes, correlate with recent deploys, and look at logs around the slow path. Narrow it to a layer. If it's the database, look for N+1 queries, missing indexes, and connection-pool exhaustion. If it's CPU, take a thread dump or a profiler sample. If it's a downstream call, check timeouts and whether a slow dependency is backing up threads.
Then fix at the right layer and confirm with a measurement, not a hunch. The technical sequence matters more than any single tool: observe, hypothesize, isolate the layer, verify, fix, confirm.
Key point: The methodology is; connection pools and N+1 are the Spring-specific evidence you know where to look.
Clarify scale and requirements first (read-heavy, low latency on redirect, custom aliases, expiry). Then the core: a POST endpoint that takes a long URL, generates a short code (base62 of an incrementing id, or a hash with collision handling), stores the mapping, and returns the short URL. A GET on the code looks up the mapping and issues a 301/302 redirect.
Because redirects are the hot path, cache the code-to-URL mapping in Redis so most reads never hit the database. Discuss the edges: base62 encoding to keep codes short, a unique constraint to guard collisions, rate limiting on creation, and analytics captured asynchronously so they don't slow the redirect. Structuring the answer (clarify, API, storage, hot-path caching, edges) is what's really evaluated.
@RestController
public class LinkController {
@PostMapping("/api/links")
ShortLink create(@RequestBody @Valid CreateLink req) {
return service.shorten(req.url());
}
@GetMapping("/{code}")
ResponseEntity<Void> resolve(@PathVariable String code) {
String url = service.resolve(code); // Redis first, DB on miss
return ResponseEntity.status(HttpStatus.FOUND)
.location(URI.create(url)).build();
}
}The most common opening comparison is Spring Boot against the plain Spring Framework it builds on, and against a bare Java web app on an external server. Spring Boot doesn't replace Spring; it configures Spring for you. It picks defaults, wires beans from the classpath, and embeds a server so there's nothing to deploy separately. Plain Spring gives you the same core container but leaves the wiring and server setup to you, which is more control and more work. Knowing why Boot exists, and when its opinions get in your way, is itself an interview signal.
| Aspect | Spring Boot | Spring Framework | Plain Java + external server |
|---|---|---|---|
| Configuration | Auto-configured from classpath | Manual (Java config or XML) | Fully manual, wire everything yourself |
| Server | Embedded (Tomcat by default) | Bring your own | Deploy a WAR to Tomcat/JBoss |
| Dependency setup | Starter POMs bundle what you need | Pick each dependency and version | Pick and manage every jar |
| Time to first endpoint | Minutes | Hours of setup | Most setup of the three |
| Control vs convenience | Convenience, defaults you can override | Full control, more boilerplate | Most control, least productivity |
Prepare in layers, and practice out loud. Most Spring Boot rounds move from concept questions to live coding to a design or debugging discussion, so rehearse each stage rather than only reading answers.
The typical Spring Boot interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These Spring Boot questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works