Lessons available in both languages
Java Backend · Interview Prep

Spring Core & Dependency Injection interview questions & answers

205+ real Spring Core & Dependency Injection interview questions with model answers, plus free lessons to learn the concepts. Prepare in English & Hinglish, then practise with an AI mock interview.

18 topics · 205+ questions

How Hirenix teaches

One chapter. 90 minutes.
Interview-ready.

Every concept starts with a real-world problem — the kind that actually shows up in production code. Nothing to cram; it just clicks. Every question comes with a model answer: exactly what to say in the room, and why. Then an AI mock interview on the same chapter.

  • 📖Concept in 5 minutesNo jargon — straight to the point
  • 🛠️Real-world problemThe kind production code throws at you
  • 💬Model answerExactly what to say in the room
  • 🧠FlashcardsRevise in 10 minutes
  • 🤖AI mock interviewIt asks follow-ups too
  • 📊Weak topicsSee exactly where you're stuck
Start this chapter — free🌐 English🇮🇳 Hinglish
A student learning an interview concept on Hirenix at home
Video playlistbuilt around a syllabus18h+
Hirenix chapterbuilt around interviews90 min

The difference isn’t the content — it’s the filter. Only what’s actually used in production and actually asked in interviews. Textbook topics the industry never touches don’t make the cut.

Lessons available in both languages

What you’ll learn

  • Why Spring: IoC and the Dependency Inversion Principle
  • Dependency injection: constructor vs setter vs field
  • The IoC container: BeanFactory and ApplicationContextFree account
  • Bean definitions: XML, Java config, and component scanFree account
  • Bean scopes: singleton, prototype, and web scopesFree account
  • Bean lifecycle: PostConstruct, PreDestroy, InitializingBeanFree account
  • Autowiring, ambiguous beans, Qualifier and PrimaryFree account
  • Component scanning and stereotypes: Component, Service, RepositoryFree account
  • Java config (Configuration/Bean) vs annotation configFree account
  • Property sources, Value, and ConfigurationPropertiesFree account
  • Spring Profiles: environment-specific beansFree account
  • The circular dependency problem and LazyFree account
  • AOP basics: cross-cutting concerns and proxiesFree account
  • BeanPostProcessor vs BeanFactoryPostProcessorFree account
  • RecapFree account
  • Project: Configurable Notification Service
  • Project: Pluggable Discount Strategy with DIFree account
  • Project: Lifecycle-Audited Resource PoolFree account

Why Spring: IoC and the Dependency Inversion Principle

You have written new a thousand times. Spring's entire first lesson is about the small number of places where writing it is a mistake.

Look at a class that sends an order confirmation. Inside OrderService, someone wrote private final MessageSender sender = new EmailSender();. It works. It ships. Then the product decides orders confirm by SMS. Now you must open OrderService — a class that knows nothing about email or SMS, whose job is orders — and edit it. That edit has nothing to do with orders. That is tight coupling: OrderService did not just use a collaborator, it chose one, and choosing welds the two together.

The second cost is quieter and worse. To unit-test OrderService you must now send a real email, because the object it talks to is hardcoded inside it. There is no seam to slip a fake through.

Inversion of Control (IoC) is the fix, and the name says exactly what moves. Normally a class controls which concrete collaborators it gets — it calls new. Invert that: the class declares what it needs and someone else decides what to hand over. Dependency Injection (DI) is the specific way this is done — the dependency is passed in, usually through the constructor. IoC is the principle; DI is the technique. Interviewers ask you to separate those two words, so keep them separate.

A third term gets confused with these constantly. The Dependency Inversion Principle (the D in SOLID) is a design rule: depend on abstractions, not concretions — OrderService should reference the MessageSender interface, not EmailSender. Dependency Injection is the mechanism that delivers a concrete instance at runtime. You can follow the principle with no framework at all, by passing an interface into a constructor by hand. Spring does not give you the principle; it automates the wiring once you have followed it.

So what is Spring's container actually for? It is an object that reads your configuration, builds your objects, works out what depends on what, and hands each one its collaborators. Your code goes back to describing what it needs, and one place — the configuration — describes what it gets.

When to reach for a container: when the same object graph is built in more than one place, when what you wire changes by environment (a real payment gateway in production, a stub in test), or when you want to unit-test a class without dragging its whole dependency tree along. In the demo below, switching from email to SMS touches only the configuration; OrderService is not opened at all.

Trade-off: a container is indirection, and indirection has a real price. Wiring errors move from compile time to startup time, stack traces get deeper, and a newcomer must learn the framework before they can read the wiring. For a small script, a single throwaway tool, or a class with one dependency that will never change, plain new is the better engineering — Spring is not the goal, swappability and testability are. Notice line C in the demo: a constructor-injected class can be built by hand with a lambda as a fake, with no container anywhere. That is the payoff, and it is available whether or not Spring is in the project.

interface MessageSender { String send(String msg); }
class EmailSender implements MessageSender { public String send(String m){ return "EMAIL: " + m; } }
class SmsSender   implements MessageSender { public String send(String m){ return "SMS: " + m; } }

// BEFORE: OrderService decides for itself who its collaborator will be.
class TightlyCoupledOrderService {
    private final MessageSender sender = new EmailSender();   // <-- the problem
    String placeOrder(String item){ return sender.send("Order placed: " + item); }
}

// AFTER: OrderService only asks. Who supplies it is not its problem.
class OrderService {
    private final MessageSender sender;
    OrderService(MessageSender sender){ this.sender = sender; }
    String placeOrder(String item){ return sender.send("Order placed: " + item); }
}

@Configuration
class AppConfig {
    @Bean MessageSender messageSender(){ return new SmsSender(); }
    @Bean OrderService orderService(MessageSender s){ return new OrderService(s); }
}

public class T01 {
    public static void main(String[] a){
        System.out.println("A tight-coupled  : " + new TightlyCoupledOrderService().placeOrder("Laptop"));
        System.out.println("    to switch to SMS you must edit OrderService itself");

        try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
            OrderService svc = ctx.getBean(OrderService.class);
            System.out.println("B container-wired: " + svc.placeOrder("Laptop"));
            System.out.println("    OrderService was never opened - only the config changed");
        }

        OrderService unitTestable = new OrderService(m -> "FAKE: " + m);
        System.out.println("C no container    : " + unitTestable.placeOrder("Laptop"));
    }
}

Dependency injection: constructor vs setter vs field

Spring can hand a dependency to your object in three places: through the constructor, through a setter, or straight into a field by reflection. All three work. Only one of them is the right default, and the interview question is always why.

Constructor injection takes the dependency as a constructor parameter. Two consequences follow immediately and neither is cosmetic. First, the field can be final — the object is fully formed the instant it exists and can never be half-built afterwards. Second, the dependency is visible in the constructor signature, so the class is honest: a constructor with six parameters is a class doing six things, and you can see it without opening the body. That visibility is a feature. Field injection hides the same six dependencies and lets the class quietly grow.

Setter injection takes it through a setter after a no-argument constructor has already run. So there is a window in which the object exists but its collaborator is still null. That is the cost, and it is also the reason setter injection exists: it is the right tool when the dependency is genuinely optional, or when it may legitimately be replaced later.

Field injection@Autowired written directly on a private field — is the shortest to type and the one to avoid. Spring sets it by reflection, reaching past private and past the absence of any setter. Three things break. The field cannot be final. The dependency appears in no signature, so nothing stops the class accumulating ten of them. And the class becomes untestable without a container: new FieldInjected() compiles happily and then throws NullPointerException the moment you use it, which is exactly what line B of the run below shows.

That last point is the one that persuades people. Look carefully at what the demo proves: inside the container all three behave identically — same log line, no difference at all. The difference only appears when you take the object out of the container, which is precisely what a unit test does. Field injection looks free right up to the moment you try to test it.

When to use which: constructor injection for every mandatory dependency, which is nearly all of them. Setter injection for a genuinely optional dependency, or one you intend to swap at runtime. Field injection in production code: effectively never — its only defensible home is a throwaway test fixture or sample code where brevity beats everything.

One convenience worth knowing because it appears in every modern codebase: if a class has exactly one constructor, @Autowired on it is optional. Spring uses that constructor automatically. This is why so much real Spring code has no @Autowired anywhere and people wrongly conclude it is not using DI — it is, through the only constructor available.

Trade-off / the honest limit of constructor injection: it is the reason a circular dependency between two beans becomes a hard startup failure, while field injection would have quietly resolved it. That is not constructor injection being worse — it is the cycle being a real design problem that constructor injection refuses to hide. The circular-dependency-problem topic runs exactly that experiment. Also, a constructor with many parameters can feel noisy; the correct response is to treat the noise as the signal it is and split the class, not to switch to field injection to silence it.

interface AuditLog { void write(String s); }
class ConsoleAudit implements AuditLog { public void write(String s){ System.out.println("      audit> " + s); } }

class ConstructorInjected {
    private final AuditLog log;                       // can be final
    ConstructorInjected(AuditLog log){ this.log = log; }
    void use(){ log.write("constructor-injected ran"); }
}
class SetterInjected {
    private AuditLog log;                             // cannot be final
    @Autowired void setLog(AuditLog log){ this.log = log; }
    void use(){ log.write("setter-injected ran"); }
}
class FieldInjected {
    @Autowired private AuditLog log;                  // not final, not in any signature
    void use(){ log.write("field-injected ran"); }
}

@Configuration class Cfg {
    @Bean AuditLog auditLog(){ return new ConsoleAudit(); }
    @Bean ConstructorInjected c(AuditLog l){ return new ConstructorInjected(l); }
    @Bean SetterInjected s(){ return new SetterInjected(); }
    @Bean FieldInjected f(){ return new FieldInjected(); }
}

public class T02 {
    public static void main(String[] a){
        try (var ctx = new AnnotationConfigApplicationContext(Cfg.class)) {
            System.out.println("A inside the container all three work identically:");
            ctx.getBean(ConstructorInjected.class).use();
            ctx.getBean(SetterInjected.class).use();
            ctx.getBean(FieldInjected.class).use();
        }
        System.out.println("B now WITHOUT the container, the way a unit test builds them:");
        new ConstructorInjected(s -> System.out.println("      fake> " + s)).use();
        try {
            new FieldInjected().use();
        } catch (Exception e) {
            System.out.println("      field-injected: " + e.getClass().getSimpleName()
                             + " - outside the container this object is born incomplete");
        }
    }
}

Project: Configurable Notification Service

This is the chapter's entry project, and it is deliberately small: one service that sends notifications, wired three different ways by three different mechanisms you have already met separately. The point is to see topics 2, 7 and 11 acting on the same object graph at once, because that is what real Spring code looks like — you rarely use one of these features in isolation.

The problem. A NotificationService must send a message. In development it should print to the console; in production it must go out by SMS. It should also be able to broadcast across every channel that is available in the current environment. And no consumer of the service should have to know any of this.

How the three ideas combine.

@Profile decides which channels exist at all. ConsoleChannel and EmailChannel are annotated @Profile("!prod"), so they simply do not exist in production. SmsChannel is @Profile("prod") and exists only there. Notice this is expressed as an absence, not a condition — there is no if anywhere in the service asking which environment it is in.

@Primary decides which one wins when a single channel is asked for. Each profile has its own @Primary bean: ConsoleChannel in development, SmsChannel in production. So NotificationService can take a plain NotificationChannel parameter and always get the sensible default for wherever it is running.

Constructor injection with a List gives the service the second capability. List<NotificationChannel> all receives every channel active in the current profile, which is what broadcast iterates. Look at the output: in the dev-like run the list has two entries (console and email); under prod it has one (sms). The same code, the same method, different contents — decided entirely by wiring.

Read the run carefully, because one line does the teaching. In run A the broadcast list is [console, email], and in run B it is [sms]. Nothing in NotificationService changed. There is no configuration flag inside it, no environment lookup, no branch. The service asked for "the default channel" and "all channels", and the container answered differently in the two environments.

Why this shape is worth copying. Adding WhatsApp support is now: write a WhatsAppChannel class, annotate it with @Component and the right @Profile, and stop. NotificationService is not opened, no configuration is edited, and broadcast picks it up automatically because the list injection is what it is. That is the payoff why-spring promised, made concrete.

Two things to notice that are easy to miss. First, both primary and all are injected through the same constructor — one asks for one bean and one asks for the collection of that type, and Spring resolves them differently without any hint from you. Second, @Primary affects only the single-value injection; the List gets every match regardless, which is exactly the behaviour autowiring-and-qualifier demonstrated.

Build it yourself before reading the code below. Start with the interface and two implementations, get the service compiling with just the single-channel injection, and only then add the List. Add the profiles last — running it once with no profile and once with prod is what makes the mechanism obvious.

interface NotificationChannel { String deliver(String to, String body); }

@Component("emailChannel") @Profile("!prod")
class EmailChannel implements NotificationChannel {
    public String deliver(String to, String body){ return "[email -> " + to + "] " + body; }
}
@Component("smsChannel") @Profile("prod") @Primary
class SmsChannel implements NotificationChannel {
    public String deliver(String to, String body){ return "[sms -> " + to + "] " + body; }
}
@Component("consoleChannel") @Profile("!prod") @Primary
class ConsoleChannel implements NotificationChannel {
    public String deliver(String to, String body){ return "[console -> " + to + "] " + body; }
}

@Service
class NotificationService {
    private final NotificationChannel primary;      // whatever @Primary says for this profile
    private final List<NotificationChannel> all;    // every channel active in this profile

    NotificationService(NotificationChannel primary, List<NotificationChannel> all) {
        this.primary = primary;
        this.all = all;
    }
    String notifyOne(String to, String body){ return primary.deliver(to, body); }
    List<String> broadcast(String to, String body){
        List<String> out = new ArrayList<>();
        for (NotificationChannel c : all) out.add(c.deliver(to, body));
        return out;
    }
}

@Configuration @ComponentScan(basePackageClasses = P16.class) class Cfg {}

public class P16 {
    static void boot(String label, String... profiles){
        var ctx = new AnnotationConfigApplicationContext();
        if (profiles.length > 0) ctx.getEnvironment().setActiveProfiles(profiles);
        ctx.register(Cfg.class);
        ctx.refresh();
        NotificationService svc = ctx.getBean(NotificationService.class);
        System.out.println(label);
        System.out.println("    notifyOne  -> " + svc.notifyOne("waquar@example.com", "Your order shipped"));
        System.out.println("    broadcast  -> " + svc.broadcast("waquar@example.com", "Your order shipped"));
        ctx.close();
    }
    public static void main(String[] a){
        boot("A no profile (dev-like):");
        boot("B active = prod       :", "prod");
    }
}

Spring Core & Dependency Injectioninterview questions & answers

10 sample questions below — 205+ in the full bank inside.

A @Bean method other() is annotated @Bean(name = "utcClock"). What is the bean called?

utcClock. The explicit name overrides the method name entirely, so containsBean("other") is now false — the method name is not kept as an alias. This is worth checking rather than assuming, because it means adding an explicit name to an existing @Bean method silently breaks anything that referred to the old, method-derived name.

In simple terms: The trap is assuming the method name survives as an additional name. Knowing that it does not is the kind of detail you only have if you have actually looked at containsBean output rather than reasoned from the annotation.

Why does a bean need lifecycle callbacks at all when it has a constructor?

Because a constructor cannot always finish the job. A bean may need to do work after its dependencies have arrived — open a connection, warm a cache, validate that the configuration it was given actually makes sense — and when the application shuts down it may need to release what it opened. Those two moments are what the callbacks are for. The key fact underneath is that dependency injection has already happened by the time the initialisation callback runs, which a setter-injected or field-injected constructor cannot rely on.

In simple terms: Framing the answer around when dependencies are available is what makes the rest of the topic follow. It also sets up the honest observation that constructor injection removes much of the need for an initialisation hook.

What does @Primary do?

It marks one bean as the default answer for its type. It is a property of the bean: declare it once and every unqualified injection point for that type receives it. With two Sender beans where the SMS one is @Primary, a plain Sender parameter gets the SMS bean. It is a default, not a strict directive — it settles the question only when nobody at the injection point has said otherwise.

In simple terms: The phrase to hold onto is that @Primary is a property of the bean while a qualifier is a property of the injection point. That distinction is what makes the precedence question answerable rather than memorised.

How can a bean definition get registered?

Java configuration with @Configuration and @Bean, where the method itself is the recipe. Component scanning, where Spring finds a @Component class and infers the definition from it. Programmatic registration, where you build a BeanDefinition and register it yourself. And XML, the original way, where a <bean id="..." class="..."/> element expresses exactly the same metadata. All four express the same thing — the metadata — through different syntax.

In simple terms: The point that makes the answer coherent is the last one: these are not four different mechanisms, they are four ways of writing the same metadata, and the container sees no difference by the time definitions are registered.

What name does a component-scanned class get?

The decapitalized class name, so ScannedBean becomes scannedBean. It follows the same principle as @Bean methods — the identifier you did not choose is derived from the code you wrote — and it has the same consequence: renaming the class renames the bean, and any string qualifier pointing at the old name breaks at startup with no compile error.

In simple terms: The naming rule itself is trivia; pairing it with the rename consequence is what makes the answer useful. Note that the pattern is consistent across mechanisms, which is worth saying because it means one rule covers both.

Should you know XML configuration in 2026?

You should be able to recognise it, not write new code in it. A <bean id="..." class="..."/> element expresses exactly the same metadata that a @Bean method or a @Component does — same concept, older syntax. Interviewers still ask whether you have seen XML config because plenty of older systems run on it and someone has to maintain them. Reading it is a real skill; choosing it for a new project is not.

In simple terms: The honest framing matters here. Dismissing XML entirely reads as inexperience with legacy systems, while presenting it as a live option reads as being out of date; the correct position is recognise, do not author.

What problem does AOP exist to solve?

Concerns that refuse to live in one place. Logging, timing, security checks and transaction boundaries are each a single idea, and each ends up copy-pasted at the top and bottom of a hundred methods. Those are cross-cutting concerns, and aspect-oriented programming exists to say them once. The test for whether something qualifies is whether the logic is identical across many methods and orthogonal to what those methods actually do.

In simple terms: Ending with the test is what makes the answer usable, because the failure mode of AOP is applying it to logic that is not really uniform. Naming the classic four examples is expected, but the criterion is what shows judgement.

How does @Autowired decide which bean to inject?

By type first. Spring matches the declared type of the parameter or field against the beans it has, and only falls back to the parameter or field name when the type alone is ambiguous. That two-step rule is the whole mechanism, and it explains why almost every autowiring question in an interview is really a question about what happens when the type matches more than once.

In simple terms: Type first, name as a tie-breaker is the sentence to have ready. Candidates who say Spring matches by name have the model backwards, which then makes every ambiguity question harder than it needs to be.

How does @Qualifier resolve ambiguity?

It names the bean you want at the injection point. Where @Primary is declared once on the bean, @Qualifier("email") is written where the dependency is consumed, so the choice is local and explicit — reading the consumer tells you exactly what arrives. That explicitness is its main advantage and its main cost, since the name is a string the compiler does not check.

In simple terms: Answering with the location — at the injection point, not on the bean — is what makes the comparison with @Primary meaningful. It also leads naturally into the trade-off about unchecked string names.

What question does a bean scope answer?

One question only: when you ask the container for this bean, do you get the same object back or a new one? Singleton means one instance per container, so you always get the same object. Prototype means a new instance every time it is requested. Everything else about scopes — the web ones, the lifecycle differences — follows from that single decision about identity.

In simple terms: Reducing scope to one question keeps the candidate from reciting six names without a model. It also makes the later surprises easier to reason about, because each of them is really a question about when the container is being asked.

195+ more Spring Core & Dependency Injection questions inside

Create a free account to read the full question bank, learn every topic, and practise with an AI mock interview.

Unlock all questions — free

Ready to practise Spring Core & Dependency Injection?

Unlock every topic free, then face an AI interviewer that asks follow-ups and grades your answers.