Spring Core Interview Questions and Answers
-
Last Updated: May 31, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
Spring core interview questions are the first thing most interviewers ask when they want to test your Java backend skills. The reason is simple. Spring Core is the base of the entire Spring Framework. Spring MVC, Spring Boot, Spring Security, and Spring Data all sit on top of it. If your core is weak, everything built on it feels shaky too. So a strong grip on Spring Core gives you a real advantage in any interview.
In this article, you will learn 15 of the most common Spring Core questions, asked again and again at product companies and service companies alike. The questions move from beginner to advanced in a smooth order. Each answer is written in plain, easy English so that even if you are new to Spring, you can follow along. Every answer also includes short, working code so you can see the idea in action, not just read about it.
We will start with the basics like Inversion of Control and Dependency Injection. Then we will cover beans, the IoC container, bean scopes, and the full bean lifecycle. Finally, we will reach advanced topics like circular dependencies, initialization callbacks, and profiles. By the end, you will be able to answer these spring core interview questions with the depth that interviewers expect.
| 📌 How to use this article: Read each question as if the interviewer just asked it. Try to answer in your head first. Then read the answer and the Interview Insight at the end, which highlights the one detail that makes a candidate stand out. |
Answer – The Spring Framework is an open-source framework for building Java applications. Its main job is to manage your objects and their connections for you, so you can focus on business logic instead of plumbing code. Spring was created to make enterprise Java simpler. Before Spring, developers wrote a lot of boilerplate code just to wire objects together and handle cross-cutting concerns such as transactions, logging, and security. This made applications hard to read, hard to test, and tightly bound to specific implementations. Spring removed most of that pain by taking over the work of creating and connecting objects.
The biggest advantage of Spring is loose coupling. Your classes do not create their own dependencies. Instead, Spring creates them and injects them where needed. This single idea makes your code easier to test, change, and maintain over time. Because a class no longer hard-codes which implementation it uses, you can swap one implementation for another, or replace a real object with a mock in tests, without touching the class itself. Other advantages include built-in support for transactions, simple integration with databases and messaging systems, a powerful web layer, and a huge ecosystem of related projects such as Spring Boot and Spring Security.
Spring is organized into modules, so you only use what you need. The Core Container contains the most important parts: Core, Beans, Context, and the Spring Expression Language (SpEL). These provide the IoC container and dependency injection. On top of that sit modules for data access and transactions, for the web and Spring MVC, for aspect-oriented programming, and for testing. This modular design means a small project can pull in just the core, while a large enterprise application can add exactly the modules it requires and nothing more.
// A simple Spring-managed class
@Component
public class GreetingService {
public String greet(String name) {
return "Hello, " + name + "!";
}
}
// Spring creates this object and gives it to us — we never call "new"
@Component
public class AppRunner {
private final GreetingService greetingService;
public AppRunner(GreetingService greetingService) {
this.greetingService = greetingService; // injected by Spring
}
}
In the code above, we never write a new GreetingService(). Spring builds the object, stores it, and passes it where it is needed. This is the heart of what the Spring Framework does for you, and it is why Spring code stays clean and flexible as a project grows.
| 🎯 Interview Insight: If asked ‘what problem does Spring solve?’, do not just list features. Say one clear line: Spring removes the tight coupling between classes by taking control of object creation and wiring. That single sentence shows you understand the why, not just the what. |
Answer – Inversion of Control, or IoC, is a design principle. Normally, your code is in control. It decides when to create objects and how to connect them. With IoC, you hand that control over to a framework. The framework now decides when objects are created and how they are wired together. The control has been inverted, which is exactly where the name comes from. Instead of your objects pulling in what they need, the framework pushes the needed objects into them.
Dependency Injection, or DI, is the most common way to achieve IoC. DI means an object does not build its own dependencies. Instead, something outside the object supplies, or injects, them. In Spring, that something is the IoC container. So the two ideas are related but not identical. IoC is the broad principle of giving up control of object creation, while DI is the specific, practical technique Spring uses to apply that principle. You can think of DI as one concrete way to implement the wider IoC idea.
Think of it like ordering food at a restaurant. Without IoC, you walk into the kitchen and cook the meal yourself, gathering every ingredient and tool. With IoC and DI, you simply place an order, and the kitchen prepares the dish and brings it to your table. You receive exactly what you need without having to make it. In the same way, a Spring bean declares its dependencies through its constructor, and the container delivers them ready to use.
// WITHOUT DI — the class creates its own dependency (tight coupling)
public class OrderService {
private PaymentService paymentService = new PaymentService(); // hard-coded
}
// WITH DI — Spring injects the dependency (loose coupling)
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService; // given to us by Spring
}
}
In the first example, OrderService is locked to one exact PaymentService and cannot be changed without editing the class. In the second, Spring decides which PaymentService to provide, so you can swap it for a mock in tests or for a different implementation in production, all without changing OrderService at all. That flexibility is the practical payoff of IoC delivered through DI.
| 🎯 Interview Insight: A classic trap question: ‘Are IoC and DI the same thing?’ The clean answer is no. IoC is the principle of giving up control of object creation. DI is one specific pattern that implements IoC. Spring uses DI to deliver IoC. |
Answer – Spring supports three ways to inject dependencies into a bean. Constructor injection passes dependencies to the class constructor when the object is created. Setter injection passes them through public setter methods after the object has already been created. Field injection places the dependency directly into a field using the @Autowired annotation, with no constructor or setter involved at all. All three end with the bean holding its dependencies, but they differ in timing, safety, and ease of testing.
Constructor injection is the recommended approach, and the Spring team itself prefers it. The reasons are practical. First, the object is fully built and valid the moment it is created, because all required dependencies must be supplied at construction time. There is no window where the object exists but is half-wired. Second, you can mark the fields final, which makes the object immutable and naturally thread-safe. Third, it makes unit testing very easy, because you simply pass the dependencies into the constructor in your test, without needing Spring or reflection at all. These three benefits together make constructor injection the safest default.
Setter injection is useful for optional dependencies that may or may not be present, because a setter can simply be left uncalled. It is also handy when you need to reconfigure a bean after creation.
Field injection looks short and clean and is often seen in tutorials, but it has real drawbacks. It hides a class’s dependencies from anyone reading the constructor, it cannot be used with final fields, and it makes testing harder because you must use reflection or a full Spring context to set the field. For these reasons, field injection is generally discouraged in production code, even though it remains common in examples and quick demos.
A simple way to remember the guidance is this: use constructor injection for anything the bean cannot work without, use setter injection for the rare optional dependency, and avoid field injection in code you intend to maintain and test seriously.
// 1. Constructor injection (RECOMMENDED)
@Service
public class ReportService {
private final DataRepository repository;
public ReportService(DataRepository repository) {
this.repository = repository;
}
}
// 2. Setter injection (good for optional dependencies)
@Service
public class ReportService2 {
private DataRepository repository;
@Autowired
public void setRepository(DataRepository repository) {
this.repository = repository;
}
}
// 3. Field injection (discouraged — hard to test, no final fields)
@Service
public class ReportService3 {
@Autowired
private DataRepository repository;
}
| Type | Best For | Can Be final? | Testability |
| Constructor | Required dependencies | Yes | Excellent |
| Setter | Optional dependencies | No | Good |
| Field | Quick code (avoid) | No | Poor |
| 🎯 Interview Insight: If you have only one constructor, Spring 4.3 and later inject into it automatically, so you do not even need @Autowired on it. Mentioning this shows you are current with modern Spring style. |
Answer – A Spring Bean is simply an object that the Spring IoC container creates, manages, and connects for you. The word bean just means a managed object, nothing more mysterious than that. You do not call new to make a bean. Instead, you tell Spring about the class, and Spring builds the object, keeps it, and hands it out wherever it is needed. Any normal Java class can become a bean, which means there is nothing special about the class itself. What makes it a bean is the fact that the container is responsible for its life.
The IoC container is the part of Spring that does all this work. It reads your configuration, which can be annotations like @Component on your classes, or @Bean methods inside a class marked with @Configuration. From this configuration, the container learns which objects it must create. It then creates each object, injects the dependencies that object needs, manages its full lifecycle from start to finish, and finally cleans it up when the application shuts down. In short, the container owns the objects from birth to destruction.
You can picture the container as a combined smart factory and storage room. It builds your objects once, stores them, and gives the same instance back whenever someone asks, unless you tell it to behave differently with a scope. This central management is the reason your classes stay loosely coupled and easy to test. Because the container, not your code, decides how objects are made and wired, you can change those decisions in one place. Two common ways to register a bean are shown below, and both put the object fully under the container’s control.
// Way 1: mark a class as a bean with a stereotype annotation
@Component
public class EmailService { }
// Way 2: define a bean with a @Bean method
@Configuration
public class AppConfig {
@Bean
public EmailService emailService() {
return new EmailService();
}
}
// Getting a bean from the container
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
EmailService service = context.getBean(EmailService.class);
Both ways above register an EmailService bean. Once it is registered, the container owns it completely. You ask the container for the bean, and it returns the managed instance it already built for you, fully wired and ready to use.
| 🎯 Interview Insight: Be ready for the follow-up: ‘Is every Java object a bean?’ No. An object is a bean only if the Spring container manages it. An object you create yourself with new is a plain object, not a Spring bean. |
Answer – Both BeanFactory and ApplicationContext are Spring IoC containers, and both can create and manage beans. The difference is that ApplicationContext is the bigger, more powerful one. In fact, ApplicationContext extends BeanFactory, so it inherits everything BeanFactory can do and then adds many extra features on top. Because of this relationship, you can correctly describe ApplicationContext as a superset of BeanFactory, which is a phrase interviewers like to hear.
BeanFactory is the most basic container. It provides core dependency injection and keeps things lightweight, which historically mattered on memory-limited devices. By default, it creates beans lazily, meaning a bean is built only when you first ask for it. This saves memory at startup, but it has a downside: if a bean is misconfigured, you may not discover the problem until much later, when the bean is finally requested. In a real application, delays can hide serious errors until they suddenly appear in production.
ApplicationContext adds the features that real applications need every day. It supports publishing and listening to events, internationalization for translating messages, easy loading of files and other resources, and automatic detection of bean post-processors. Just as importantly, it creates singleton beans eagerly at startup by default. This means any configuration problem shows up immediately when the application starts, rather than later during normal operation. For almost every modern Spring or Spring Boot application, ApplicationContext is the correct choice, and it is exactly what Spring Boot uses behind the scenes when your application starts.
In practice, you will almost never create a BeanFactory by hand anymore. When you start a Spring Boot application, an ApplicationContext is created for you automatically, and every bean, event, and resource feature is available from that point on. It is still worth understanding the relationship between the two because it explains why Spring behaves as it does. The short version to give in an interview is that BeanFactory is the lightweight, lazy foundation, while ApplicationContext is the rich, eager container built on top of it that you should actually use in real systems.
// BeanFactory — basic, lazy by default (rarely used directly now)
BeanFactory factory =
new XmlBeanFactory(new ClassPathResource("beans.xml")); // legacy
// ApplicationContext — full-featured, eager singletons, recommended
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
MyService service = context.getBean(MyService.class);
| Feature | BeanFactory | ApplicationContext |
| Core DI | Yes | Yes |
| Bean creation | Lazy by default | Eager singletons by default |
| Event publishing | No | Yes |
| Internationalization (i18n) | No | Yes |
| Recommended for apps | No | Yes |
| 🎯 Interview Insight: A great line to drop: ‘ApplicationContext is a superset of BeanFactory.’ Then add that eager singleton creation in ApplicationContext means you fail fast at startup if a bean is misconfigured, which is exactly what you want in production. |
Answer – @Autowired is an annotation that tells Spring to inject a dependency automatically. You place it on a constructor, setter, or field, and Spring finds a matching bean in the container and supplies it for you. This saves you from wiring objects by hand and is the most common way to connect beans in modern Spring code. Without it, you would have to manually look up and pass every dependency, which is exactly the kind of boilerplate Spring was built to remove.
By default, @Autowired resolves dependencies by type. Spring looks at the type you asked for and searches the container for a bean of that exact type. If it finds exactly one match, it injects that bean, and the work is done. Problems begin when there are two or more beans of the same type, because then Spring does not know which one you want. In that situation, it throws a NoUniqueBeanDefinitionException at startup, telling you the choice is ambiguous and needs to be made explicit by you.
To resolve that ambiguity, Spring can fall back to name-based matching. If several beans match the required type, Spring tries to match the field name or the constructor parameter name to a bean name. If a name matches, that bean is chosen.
You can also take control directly. Use @Qualifier to name the exact bean you want at the injection point, or mark one bean with @Primary so it becomes the default winner whenever the choice is otherwise unclear. These tools let you resolve any ambiguity precisely. The overall rule to remember is that Spring tries byType first, then uses the name, @Qualifier, or @Primary to break a tie.
// Two beans of the same type
@Component("fastService") class FastService implements Notifier { }
@Component("slowService") class SlowService implements Notifier { }
// byType fails here (two matches), so we guide Spring:
// Option A: @Qualifier names the exact bean
@Service
public class AlertService {
private final Notifier notifier;
public AlertService(@Qualifier("fastService") Notifier notifier) {
this.notifier = notifier;
}
}
// Option B: matching the field name to the bean name
@Autowired
private Notifier fastService; // matches the bean named "fastService"
In short, Spring resolves by type first and uses the bean name as a fallback when more than one candidate exists. Knowing this exact order, and the role of @Qualifier and @Primary, is precisely what interviewers want to hear on this question.
| 🎯 Interview Insight: Watch out for required vs optional. By default @Autowired is required, so startup fails if no matching bean is found. Set @Autowired(required = false), or use Optional or @Nullable, when the dependency may be absent. |
Answer – These three annotations all help register beans, but they are used in different situations and operate at different levels. @Component marks one of your own classes as a bean so Spring can find it during classpath scanning. @Bean is a method-level annotation used inside a configuration class to register the object that the method returns. @Configuration marks a class that holds one or more @Bean methods and tells Spring to treat it as a source of bean definitions. Understanding which to reach for is mostly about whether you own the class you want to register.
Use @Component when you own the class and can put an annotation directly on it. This is the everyday case for your services, repositories, and controllers. Spring scans your packages, finds every class annotated with @Component, and registers each one automatically as a bean. The familiar variations, such as @Service, @Repository, and @Controller, do the same job but add clearer meaning about the role of the class. Because of scanning, you usually do not write any extra code: marking the class is enough for it to become a managed bean that Spring can inject elsewhere.
Use @Bean when you cannot annotate the class, for example, a class that comes from a third-party library, or when you need custom logic to build and configure the object before handing it over. Since you cannot add @Component to code you do not own, you write a @Bean method inside a @Configuration class, build the object yourself, and return it. Spring then manages whatever the method returns. So the simple rule of thumb is this: use @Component for your own classes, and use @Bean for objects you must construct by hand or that arrive from outside your codebase. @Configuration is the container that groups @Bean methods. Keeping this rule in mind makes configuration decisions almost automatic as your project grows and you mix your own classes with library types.
// @Component — your own class, auto-detected by scanning
@Component
public class InvoiceService { }
// @Configuration + @Bean — build a third-party object yourself
@Configuration
public class AppConfig {
@Bean
public ObjectMapper objectMapper() { // from a library
ObjectMapper mapper = new ObjectMapper();
mapper.findAndRegisterModules();
return mapper; // Spring manages this
}
}
| Annotation | Level | Use When |
| @Component | Class | You own the class and can annotate it |
| @Bean | Method | Building a third-party or custom object |
| @Configuration | Class | A class that holds @Bean methods |
| 🎯 Interview Insight: A subtle point: @Bean methods inside a @Configuration class are special. Spring uses a CGLIB proxy so that calling one @Bean method from another returns the same singleton, not a brand-new object. This is a favorite advanced follow-up. |
Answer – Both @Qualifier and @Primary solve the same underlying problem: what to do when more than one bean of a type exists, and Spring cannot decide which one to inject. They solve it from two different sides, and understanding that difference is the key to this question. @Primary sets a default winner that applies everywhere, while @Qualifier names the exact bean you want at one specific injection point. One is global, the other is local, and that distinction is the heart of a strong answer.
@Primary is placed on a bean definition. It tells Spring that if there are multiple candidates of the same type and no one asks for a specific one, this bean should be chosen. It is ideal when one implementation is the usual choice in most places, and the alternatives are needed only occasionally. You set @Primary once on the preferred bean, and from then on, every plain injection of that type quietly receives the primary bean. This keeps your wiring code clean because most injection points need no extra annotation at all, and the intent of a single default is obvious to readers.
@Qualifier is placed at the injection point, right next to @Autowired or on a constructor parameter. It says, ignore the default and give me this specific named bean. You use it when one particular class needs a non-default implementation. The two annotations work together very naturally: @Primary handles the common case across the application, and @Qualifier overrides it in the few places that need something else. When both apply to the same injection, @Qualifier wins, because it is the more specific and more explicit instruction. That precedence is a detail interviewers often probe, so state it clearly. A good mental model is to treat @Primary as the answer to the question of what should happen by default, and @Qualifier as the answer to the question of what should happen right here, with the local answer always overriding the global one.
public interface PaymentGateway { }
@Component @Primary // default choice everywhere
public class StripeGateway implements PaymentGateway { }
@Component("paypalGateway")
public class PaypalGateway implements PaymentGateway { }
@Service
public class CheckoutService {
// No qualifier -> gets StripeGateway because it is @Primary
public CheckoutService(PaymentGateway gateway) { }
}
@Service
public class RefundService {
// @Qualifier overrides @Primary -> gets PaypalGateway
public RefundService(@Qualifier("paypalGateway") PaymentGateway gateway) { }
}
| Aspect | @Primary | @Qualifier |
| Where it goes | On the bean | At the injection point |
| Meaning | Default choice | Pick this exact bean |
| Best for | One common default | Specific overrides |
| Wins if both used | No | Yes |
| 🎯 Interview Insight: Say this and you sound senior: ‘@Primary defines a global default; @Qualifier makes a local, explicit choice. @Qualifier always beats @Primary because the more specific instruction wins.’ |
Answer – A bean scope controls how many instances of a bean Spring creates and how long each instance lives. Choosing the right scope matters because it directly affects memory usage and whether beans can safely share state between callers. If you pick the wrong scope, you can either waste memory or, worse, accidentally share data that should have stayed separate per user or per request. Spring offers several scopes, and you select one using the @Scope annotation on the bean.
Singleton is the default scope and the one you will use most often. Spring creates exactly one instance of the bean for the entire container and returns that same shared instance every time the bean is requested. This is perfect for stateless services, which contain logic but no per-user data that changes.
Prototype is the opposite. Spring creates a brand-new instance every single time the bean is requested. This suits objects that carry state unique to a single use, where sharing a single instance would cause different callers to overwrite each other’s data and produce incorrect results.
The remaining scopes are web-only and apply specifically to web applications.
Request scope creates one bean per HTTP request, so each incoming request gets its own fresh instance that is discarded when the request ends.
Session scope creates one bean per user session, which lives as long as that user’s session does, and is useful for storing per-user data across multiple requests.
Application scope creates a single bean for the entire ServletContext, shared across the whole web application, similar to a singleton but tied to the servlet container rather than the Spring container.
You choose one of these when a bean must live for exactly the length of a request, a session, or the application. The key idea behind all scopes is to match the bean’s lifetime to the lifetime of the data it holds, so that nothing lives longer or shorter than it should.
// Singleton (default) — one shared instance
@Service
public class CurrencyService { }
// Prototype — a new instance every time it is requested
@Component
@Scope("prototype")
public class ShoppingCart { }
// Web scopes
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext { }
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserPreferences { }
| Scope | Instances Created | Typical Use |
| singleton (default) | One per container | Stateless services |
| prototype | New one each request | Stateful, per-use objects |
| request | One per HTTP request | Per-request data |
| session | One per user session | Per-user data |
| application | One per ServletContext | App-wide shared data |
| 🎯 Interview Insight: When injecting a shorter-lived bean (like request scope) into a longer-lived one (like a singleton), you need proxyMode = ScopedProxyMode.TARGET_CLASS. The proxy fetches the correct instance per request. Forgetting this is a common bug. |
Answer – Singleton beans are not automatically thread-safe. This surprises many beginners, so it is worth stating plainly. Spring guarantees only that there is exactly one instance of the bean. It does not add any protection around how that instance is used. Because the same single object is shared by many threads at the same time, any mutable state stored inside the singleton can be corrupted when several threads read and write it together. The singleton scope is about instance count, not about safety, and that distinction is the core of the answer.
The safe rule is to keep singleton beans stateless. Most service beans hold references to other beans and contain logic that does not change data, so they are naturally safe to share. Problems arise when you store request- or user-specific data in a singleton’s field, because every thread reads and writes that same field, leading to race conditions and incorrect results. If you genuinely must hold mutable state inside a singleton, you have to protect it yourself using synchronization, atomic types like AtomicInteger, or thread-local storage that gives each thread its own private copy.
The prototype-in-singleton problem is a closely related trap. Suppose you inject a prototype-scoped bean into a singleton-scoped bean. You might expect a fresh prototype every time you use it, but that is not what happens. Spring injects the prototype only once, at the moment it builds the singleton. After that, the singleton keeps the same prototype instance forever, so the prototype no longer behaves like a prototype at all. The fix is to ask the container for a new instance each time you need one, using ObjectProvider, a @Lookup method, or a JSR-330 Provider. Each of these fetches a fresh prototype on demand instead of reusing the one captured at startup. The reason the problem exists is that injection occurs once during the singleton’s creation, whereas a true prototype requires a new lookup on every use, so you must move the lookup out of the injection step and into the method that actually needs the object.
// UNSAFE singleton — shared mutable field, corrupted under load
@Service
public class CounterService {
private int count = 0; // shared by all threads!
public void increment() { count++; } // race condition
}
// Prototype-in-singleton problem
@Component @Scope("prototype")
public class Task { }
@Service
public class TaskRunner {
// Injected ONCE — same Task reused forever (bug)
// private Task task;
// FIX: get a fresh prototype on demand
private final ObjectProvider<Task> taskProvider;
public TaskRunner(ObjectProvider<Task> taskProvider) {
this.taskProvider = taskProvider;
}
public void run() {
Task freshTask = taskProvider.getObject(); // new each call
}
}
| 🎯 Interview Insight: The cleanest one-liner: ‘Spring guarantees one instance, not thread safety.’ Then explain that statelessness is your real protection, and that ObjectProvider solves the prototype-in-singleton problem by fetching a new instance on each call. |
Answer – The bean lifecycle is the full journey a bean takes from birth to death inside the container. Knowing the order matters because it tells you exactly when your dependencies are ready and when it is safe to run setup or cleanup logic. If you try to use a dependency too early, you will hit a NullPointerException, and if you release a resource at the wrong time, you can break a clean shutdown. The lifecycle follows a clear, repeatable sequence that you can rely on every time.
First, Spring instantiates the bean by calling its constructor.
Next, it injects all dependencies through fields, setters, or the constructor, depending on how you wired them.
Then it calls any Aware interface methods the bean implements, such as setBeanName or setApplicationContext, which give the bean access to container details.
After that, BeanPostProcessors run their before-initialization step.
Then the initialization callbacks fire in a fixed order: first any method annotated with @PostConstruct, then afterPropertiesSet from the InitializingBean interface, then any custom init method you configured.
Finally, BeanPostProcessors run their after-initialization step, and the bean is fully ready to use.
When the container shuts down, the destruction phase begins, but only for singleton beans.
Spring calls the @PreDestroy method first, then destroy from the DisposableBean interface, and finally any custom destroy method you configured. This sequence mirrors the initialization order and gives the bean a clean chance to release resources such as database connections, open files, or background threads. One important detail to remember is that prototype beans are not managed after they are created, so Spring never calls their destruction callbacks. Cleaning up a prototype is your responsibility, not the container’s, which is a point worth raising.
@Component
public class LifecycleBean implements InitializingBean, DisposableBean {
public LifecycleBean() {
System.out.println("1. Constructor");
}
@PostConstruct
public void postConstruct() {
System.out.println("2. @PostConstruct");
}
@Override
public void afterPropertiesSet() {
System.out.println("3. afterPropertiesSet()");
}
@PreDestroy
public void preDestroy() {
System.out.println("4. @PreDestroy");
}
@Override
public void destroy() {
System.out.println("5. destroy()");
}
}
If you run this in a real context and then close it, the messages print in exactly the order shown above. Demonstrating that visible order is a great way to prove you truly understand the sequence if an interviewer asks you to walk through it step by step.
| 🎯 Interview Insight: Remember the init order: @PostConstruct first, then afterPropertiesSet(), then custom init-method. The order of @PreDestroy, destroy(), and custom destroy-method mirrors it on shutdown. Interviewers love to test this exact sequence. |
Answer – Spring is built on top of many well-known design patterns. Knowing them shows that you understand not just how to use Spring, but how it works internally, which is exactly the kind of depth interviewers look for. You do not need to memorize a huge list to answer well. Instead, focus on the most important patterns and, crucially, be able to say where each one appears inside Spring. Naming a pattern along with its specific location is far more impressive than reciting textbook definitions without context.
The Factory pattern is everywhere in Spring, because the IoC container is essentially a bean factory that creates objects on your behalf. The Singleton pattern is available with the default bean scope, where a single instance serves the entire container. The Proxy pattern powers Spring AOP and @Transactional: Spring wraps your bean in a proxy so it can add behavior like opening a transaction, logging, or security checks around your methods without changing your code. These three patterns alone explain a large part of how Spring operates day to day, so they are the ones to mention first.
There are several more worth knowing. The Template Method pattern appears in helper classes such as JdbcTemplate and RestTemplate, which handle the repetitive boilerplate and let you supply only the parts that change, such as the SQL query or the URL. The Dependency Injection pattern is the core idea of the entire framework and the reason your classes stay loosely coupled. The Observer pattern drives Spring’s application event system, where listeners react to published events. The Front Controller pattern is used by the DispatcherServlet in Spring MVC, which receives every web request and routes it to the right handler. Each of these patterns solves a specific recurring problem in a clean, reusable way. The broader lesson is that Spring did not invent new magic; it applied proven object-oriented patterns consistently, which is part of why the framework has stayed stable and understandable for so many years.
// Template Method pattern in action — JdbcTemplate handles the
// connection, statement, and cleanup; you supply only what varies.
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public int countUsers() {
// You write only the query — boilerplate is hidden by the template
return jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM users", Integer.class);
}
}
| Pattern | Where It Appears in Spring |
| Factory | IoC container creating beans |
| Singleton | Default bean scope |
| Proxy | Spring AOP and @Transactional |
| Template Method | JdbcTemplate, RestTemplate |
| Observer | Application events |
| Front Controller | DispatcherServlet in Spring MVC |
| 🎯 Interview Insight: Do not just name patterns. Pair each with its exact place in Spring, like ‘Proxy pattern in @Transactional’ or ‘Front Controller in DispatcherServlet’. Concrete examples prove real understanding and impress interviewers. |
Answer – A circular dependency happens when two or more beans depend on each other in a loop. For example, bean A needs bean B, and bean B needs bean A. To create A, Spring must first create B, but to create B, it must first create A. This creates a chicken-and-egg situation that the container has to handle carefully. Circular dependencies can also be longer, such as A needs B, B needs C, and C needs A, but the core problem is the same: a cycle with no clear starting point for creation.
Whether Spring can resolve the cycle depends entirely on the injection type you use. With setter or field injection, Spring can break the cycle. It first creates the raw object of A by calling its constructor, then places an early reference of A into an internal cache before A is fully wired. It then creates B, injects A’s early reference into B, and finishes B. Finally, it injects the now-complete B back into A. This works because the objects already exist as plain instances before their dependencies are fully set, so an early, half-built reference can safely be shared between them.
With constructor injection, Spring cannot break the cycle. Both objects need each other to be fully built at construction time, and neither can be created first, so there is no half-built instance to share. In that case, Spring fails fast at startup and throws a BeanCurrentlyInCreationException. The best fix is almost always to redesign the code to remove the cycle, because a circular dependency usually signals poor separation of responsibilities between the two classes. If you truly cannot avoid it, you can place @Lazy on one of the dependencies, which causes Spring to inject a proxy and delay the real bean’s creation until it is first used, thereby quietly breaking the loop.
// Constructor injection cycle -> startup failure
@Component
class A {
A(B b) { } // needs B
}
@Component
class B {
B(A a) { } // needs A -> BeanCurrentlyInCreationException
}
// Fix with @Lazy — Spring injects a proxy and delays real creation
@Component
class A2 {
private final B2 b;
A2(@Lazy B2 b) { this.b = b; } // proxy now, real bean later
}
@Component
class B2 {
private final A2 a;
B2(A2 a) { this.a = a; }
}
| 🎯 Interview Insight: Key nuance: Spring resolves circular dependencies with setter/field injection but NOT with pure constructor injection. The real-world advice is to treat a cycle as a design smell and refactor it, rather than reaching for @Lazy as a permanent fix. |
Answer – Spring gives you three different ways to run setup logic after a bean is created and cleanup logic before it is destroyed. The three init options are the @PostConstruct annotation, the InitializingBean interface with its afterPropertiesSet method, and a custom init-method that you name in your configuration. The three destroy options mirror them exactly: the @PreDestroy annotation, the DisposableBean interface with its destroy method, and a custom destroy-method. Having three choices can feel confusing at first, but each one fits a slightly different situation in practice.
All three init callbacks run after dependency injection is complete, so by the time any of them runs, your dependencies are safely available. The important detail is that they run in a fixed, guaranteed order. First @PostConstruct runs, then afterPropertiesSet from InitializingBean, and finally the custom init-method. The destruction callbacks follow the same pattern when the container shuts down: @PreDestroy runs first, then destroy from DisposableBean, then the custom destroy-method. Interviewers very often ask you to recite this order, so it is well worth memorizing both the init and destroy sequences until they are automatic.
Which one should you actually pick in real code? @PostConstruct and @PreDestroy are usually preferred, because they are clean, standard Java annotations from the JSR-250 specification, and they do not tie your class to any Spring interface. That keeps your code portable and easy to read. Implementing InitializingBean or DisposableBean works fine, but it couples your class directly to Spring types, which is generally something to avoid in well-designed code. Custom init and destroy methods are most useful when you configure third-party beans through a @Bean method and cannot add annotations to their source code, since you can still point Spring at the right methods by name. In everyday work, the annotation approach covers the vast majority of cases, so a good default is to reach for @PostConstruct and @PreDestroy first and only fall back to the other two when you have a specific reason.
@Component
public class ConnectionManager
implements InitializingBean, DisposableBean {
@PostConstruct
public void init1() { System.out.println("1. @PostConstruct"); }
@Override
public void afterPropertiesSet() {
System.out.println("2. afterPropertiesSet()");
}
public void customInit() { System.out.println("3. custom init"); }
@PreDestroy
public void cleanup1() { System.out.println("4. @PreDestroy"); }
@Override
public void destroy() { System.out.println("5. destroy()"); }
public void customDestroy() { System.out.println("6. custom destroy"); }
}
// Registering the custom init/destroy methods
@Bean(initMethod = "customInit", destroyMethod = "customDestroy")
public ConnectionManager connectionManager() {
return new ConnectionManager();
}
| Approach | Init Method | Destroy Method | Couples to Spring? |
| Annotations | @PostConstruct | @PreDestroy | No (preferred) |
| Interfaces | afterPropertiesSet() | destroy() | Yes |
| Custom methods | init-method | destroy-method | No |
| 🎯 Interview Insight: Memorize the init order: @PostConstruct, then afterPropertiesSet(), then custom init-method. If the interviewer asks why you prefer @PostConstruct, say it is a standard annotation that keeps your class free from Spring-specific interfaces. |
Answer – @Profile lets you register a bean only when a certain environment, called a profile, is active. A profile is simply a named group of beans and configuration, such as dev, test, or prod. This feature is how you give different behavior to different environments without changing any code. A very common example is using a fast in-memory database during development and a real production database when the application is deployed, with the same code working in both cases, depending on which profile is enabled at startup.
You apply @Profile to a bean or to a configuration class, along with one or more profile names. That bean is then created only if a matching profile is active when the application starts. If the profile is not active, Spring simply skips that bean entirely, as if it were never declared. This keeps environment-specific objects cleanly separated from one another, so your development setup never accidentally leaks into production, and vice versa. It also makes the intent obvious to anyone reading the code because the profile name clearly indicates where each bean belongs.
You activate a profile in several ways. The most common is the property spring.profiles.active, which you can set inside application.properties, pass as a command-line argument, or supply as an environment variable. You can activate multiple profiles at the same time, allowing you to combine related groups of beans. Spring 5.1 also added support for logical profile expressions, so you can write conditions such as prod & us-east to require both, prod | staging to accept either, or !dev to mean any profile except dev. These expressions give you precise control over exactly what gets loaded in each environment, which is invaluable in larger systems with many deployment targets.
// Dev-only bean — an in-memory database
@Configuration
@Profile("dev")
public class DevDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2).build();
}
}
// Prod-only bean — a real database
@Configuration
@Profile("prod")
public class ProdDataConfig {
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://prod-db:3306/app");
return ds;
}
}
// Activate via application.properties:
// spring.profiles.active=dev
// Or via command line:
// java -jar app.jar --spring.profiles.active=prod
With the setup above, only one DataSource bean exists at any given time. Switching the active profile also switches the entire data layer, with no code changes at all. That flexibility is exactly why profiles are so widely used in real Spring applications, from small projects to large microservice systems with separate dev, staging, and production environments.
| 🎯 Interview Insight: Bonus knowledge: profile expressions support logical operators since Spring 5.1, like @Profile(“prod & us-east”) and @Profile(“!dev”). Mentioning this, plus the spring.profiles.active property, shows real depth on environment configuration. |
Spring Core is the foundation that every other Spring project stands on, which is why these Spring Core interview questions come up so often. Once you truly understand IoC and DI, beans and the container, scopes and the bean lifecycle, the rest of Spring becomes much easier to learn. The advanced topics, like circular dependencies and profiles, then feel like natural extensions rather than surprises.
Here are the key takeaways from this article: