What Spring Boot Concepts Should You Know for Java Interviews?
I thought I knew Spring Boot. Then I walked into an interview and got asked: “What happens between @PostConstruct and afterPropertiesSet?”
Blank stare. The interviewer smiled. “Be aware of its nuances,” he said, “they will catch you in an interview over it.”
That moment taught me the gap between using Spring Boot and being interview-ready for it. Here are the five areas where I got caught—and how to avoid the same traps.
1. Bean Lifecycle: The Interview Favorite
The bean lifecycle isn’t just academic trivia. Interviewers test it because real-world debugging often requires knowing exactly when initialization happens.
Here’s the complete lifecycle:
Instantiation ↓Dependency Injection ↓BeanNameAware.setBeanName() ↓BeanPostProcessor.postProcessBeforeInitialization() ↓@PostConstruct ↓InitializingBean.afterPropertiesSet() ↓BeanPostProcessor.postProcessAfterInitialization() ↓Ready for Use ↓@PreDestroy (on shutdown) ↓DisposableBean.destroy()Interview Trap: @PostConstruct runs BEFORE afterPropertiesSet. They’re not interchangeable.
Here’s a bean demonstrating all lifecycle callbacks:
@Componentpublic class BeanLifecycleDemo implements BeanNameAware, InitializingBean, DisposableBean {
private static final Logger logger = LoggerFactory.getLogger(BeanLifecycleDemo.class);
@Override public void setBeanName(String name) { logger.info("1. BeanNameAware: My name is {}", name); }
@PostConstruct public void postConstruct() { logger.info("2. @PostConstruct: Dependencies injected"); }
@Override public void afterPropertiesSet() throws Exception { logger.info("3. InitializingBean: Properties set"); }
@PreDestroy public void preDestroy() { logger.info("4. @PreDestroy: About to destroy"); }
@Override public void destroy() throws Exception { logger.info("5. DisposableBean: Destroying"); }}Why interviewers ask this: When your @PostConstruct method fails, you need to know whether it’s a dependency issue or a lifecycle timing problem. They’re testing your debugging instincts.
2. Dependency Injection: Three Patterns, One Clear Winner
I used field injection everywhere. “@Autowired on the field—done.” Then the interviewer asked me to unit test a field-injected service.
“Can’t mock that without reflection,” I realized.
Constructor Injection (Preferred)
@Servicepublic class OrderService { private final PaymentService paymentService; private final NotificationService notificationService;
// @Autowired optional since Spring 4.3 for single constructor public OrderService(PaymentService paymentService, NotificationService notificationService) { this.paymentService = paymentService; this.notificationService = notificationService; }}Pros: Immutable dependencies, easy to test, explicit requirements Cons: More verbose for many dependencies
Setter Injection (Optional Dependencies)
@Servicepublic class EmailService { private TemplateEngine templateEngine;
@Autowired public void setTemplateEngine(TemplateEngine templateEngine) { this.templateEngine = templateEngine; }}Pros: Flexible, allows optional dependencies, can be changed at runtime Cons: Mutable, risk of NullPointerException if not set
Field Injection (Avoid)
@Servicepublic class BadService { @Autowired private Dependency dependency; // Hard to test, hard to see requirements}Pros: Least code Cons: Hidden dependencies, impossible to unit test without Spring context or reflection
| Type | Immutable | Testable | Explicit | Recommendation ||-------------|-----------|----------|----------|-------------------|| Constructor | ✓ | ✓ | ✓ | Preferred || Setter | ✗ | ✓ | ✓ | Optional deps || Field | ✗ | ✗ | ✗ | Avoid |Interview tip: Since Spring 4.3, you can omit @Autowired on constructors if there’s only one constructor. They might ask why that works.
3. Stereotype Annotations: Not Just Semantics
“@Repository is just a semantic marker for the data layer, right?”
Wrong. That answer cost me points.
// Generic component - auto-detected by @ComponentScan@Componentpublic class UtilityHelper { }
// Service layer - semantically marks business logic@Servicepublic class UserService { }
// Data layer - HAS SPECIAL BEHAVIOR@Repositorypublic class UserRepository { }
// Web views - returns view names@Controllerpublic class PageController { public String home() { return "home"; }}
// REST APIs - @Controller + @ResponseBody@RestControllerpublic class ApiController { @GetMapping("/api/data") public Data getData() { return new Data(); }}The @Repository trap: It doesn’t just mark a bean as a data access object. It enables exception translation—converting SQLException to Spring’s DataAccessException hierarchy.
| Annotation | Layer | Special Behavior ||---------------|------------|-------------------------------------|| @Component | Generic | Auto-detected || @Service | Business | Semantically marks service || @Repository | Data | Exception translation (!) || @Controller | Web Views | Returns view names || @RestController| REST | @Controller + @ResponseBody |Why interviewers test this: They want to know if you understand the framework’s conventions and why they exist. Using @Service on a DAO isn’t wrong, but it shows you don’t understand Spring’s layered architecture.
4. Bean Scopes: The Prototype-in-Singleton Trap
“What happens when you inject a prototype-scoped bean into a singleton?”
“New instance each time,” I said confidently.
Wrong. You get ONE instance, injected once, reused forever.
@Scope("prototype")@Componentpublic class PrototypeBean { private String state = "initial";}
@Scope("singleton") // default@Componentpublic class SingletonBean { @Autowired private PrototypeBean prototypeBean; // Same instance forever!
public void doSomething() { prototypeBean.setState("changed"); // Affects all callers }}Correct Approaches
Option 1: Use ObjectProvider
@Componentpublic class SingletonBean { @Autowired private ObjectProvider<PrototypeBean> prototypeProvider;
public void doSomething() { PrototypeBean prototype = prototypeProvider.getObject(); // New instance! prototype.setState("fresh state"); }}Option 2: Use Proxy Mode
@Component@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)public class PrototypeBean { }| Scope | Lifecycle | Use Case | Gotcha ||----------|----------------------------|-----------------------------|---------------------------|| Singleton| One per container (default)| Stateless services | Shared state = bugs || Prototype| New per injection | Stateful beans | Injection = one instance || Request | One per HTTP request | Request-scoped data | Needs RequestContext || Session | One per user session | Shopping carts, user prefs | Cluster replication |Why this matters: In production, state leakage between requests causes bizarre bugs. Interviewers want to know you understand lifecycle implications.
5. REST API Mastery: Beyond @GetMapping
Interviewers don’t just want to see you can write endpoints. They test whether you understand the conventions and shortcuts.
@RestController@RequestMapping("/api/users")public class UserController {
private final UserService userService;
public UserController(UserService userService) { this.userService = userService; }
@GetMapping public List<User> getAllUsers() { return userService.findAll(); }
@GetMapping("/{id}") public User getUserById(@PathVariable Long id) { return userService.findById(id); }
@PostMapping @ResponseStatus(HttpStatus.CREATED) public User createUser(@Valid @RequestBody CreateUserRequest request) { return userService.create(request); }
@PutMapping("/{id}") public User updateUser(@PathVariable Long id, @Valid @RequestBody UpdateUserRequest request) { return userService.update(id, request); }
@DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteUser(@PathVariable Long id) { userService.delete(id); }}Key interview points:
@GetMapping=@RequestMapping(method = RequestMethod.GET)@PathVariableextracts values from the URL@RequestBodydeserializes JSON to objects@ResponseStatusoverrides default HTTP status@Validtriggers bean validation
Handling Multiple Beans of the Same Type
public interface PaymentProcessor { void process(); }
@Componentpublic class StripeProcessor implements PaymentProcessor { }
@Componentpublic class PayPalProcessor implements PaymentProcessor { }
@Servicepublic class PaymentService { // Option 1: @Primary on one implementation @Autowired private PaymentProcessor paymentProcessor;
// Option 2: @Qualifier @Autowired @Qualifier("stripeProcessor") private PaymentProcessor stripeProcessor;
// Option 3: Named injection @Autowired private PaymentProcessor payPalProcessor; // Matches bean name}Mark one as primary:
@Component@Primarypublic class StripeProcessor implements PaymentProcessor { }The Self-Invocation Trap
One more gotcha that catches experienced developers: transaction self-invocation.
@Servicepublic class OrderService {
@Transactional public void placeOrder(Order order) { validateOrder(order); // Transaction NOT applied! saveOrder(order); }
@Transactional(propagation = Propagation.REQUIRES_NEW) public void validateOrder(Order order) { // Self-invocation bypasses proxy - no new transaction! }}Why: Spring’s @Transactional works through proxies. Self-invocation doesn’t go through the proxy.
Solution: Inject yourself or refactor:
@Servicepublic class OrderService {
@Autowired private OrderService self; // Inject self
@Transactional public void placeOrder(Order order) { self.validateOrder(order); // Goes through proxy! saveOrder(order); }
@Transactional(propagation = Propagation.REQUIRES_NEW) public void validateOrder(Order order) { // Now creates new transaction }}Interview Mistakes to Avoid
From my interview experience, here are the mistakes that cost me points:
- Lifecycle confusion: “What runs after @PostConstruct?” → afterPropertiesSet, not @PreDestroy
- DI preference: “Field injection is simplest” → Should prefer constructor injection
- Scope misunderstanding: “Prototype in Singleton gets new instance each call” → Gets ONE instance
- @Repository misconception: “Just semantic” → Has exception translation behavior
- REST mapping: “Use @RequestMapping for everything” → Prefer specific annotations (@GetMapping, @PostMapping)
Why These Concepts Matter
Interviewers don’t ask about bean lifecycle to torture you. They ask because:
- Bug diagnosis: When your @PostConstruct fails, knowing the lifecycle tells you if it’s a wiring issue or initialization timing
- Architecture decisions: Constructor vs field injection affects testability and immutability
- Production issues: Prototype-in-singleton state leakage causes mysterious bugs
- Code quality: Using the right stereotype shows you understand layered architecture
- Team collaboration: Consistent patterns make codebases maintainable
“You’ll need to know how to navigate a project, import things safely, ensure bean checking and know how to rewire projects,” a senior architect told me. These concepts are the foundation.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments