What is Spring Framework?
Spring is a Java application framework built around one core idea: your business classes should not be responsible for finding, constructing, or wiring together the other objects they depend on. Spring does that job โ through a container that creates, configures, and connects your objects (called beans) based on configuration you provide via annotations or Java code.
Spring is not one library โ it's a modular ecosystem. Spring Core (this page) is the foundation everything else builds on: the IoC container and dependency injection mechanism. On top of that sit Spring MVC (web layer), Spring Data (persistence), Spring Security (auth), and dozens more โ each usable independently, all sharing the same underlying container and configuration model.
/*
* The Spring ecosystem, roughly:
*
* Spring Core/Beans/Context โ THIS PAGE โ IoC container, DI, bean lifecycle
* Spring AOP โ cross-cutting concerns (logging, transactions, security)
* Spring MVC โ web layer: controllers, request mapping
* Spring Data โ repositories, JPA/MongoDB/Redis integration
* Spring Security โ authentication, authorisation
* Spring Boot โ auto-configuration + embedded server + starters
* (NOT a different framework โ built ON TOP of all the above)
*/
Spring Framework is the underlying technology: IoC
container, DI, MVC, AOP, transaction management. It existed since 2003
and historically required substantial manual XML or Java configuration โ
you had to wire together a DispatcherServlet, a datasource,
a transaction manager, by hand. Spring Boot (since 2014)
doesn't replace any of that โ it auto-configures it.
spring-boot-starter-web on the classpath is enough for Boot
to infer "this is a web app" and wire an embedded Tomcat, a
DispatcherServlet, and sensible defaults automatically. Spring
Boot is covered in its own dedicated page โ everything on
this page (IoC, DI, beans, scopes) is the Spring Core
layer Boot is built on top of, and applies identically whether you use
Boot or not.
What is Inversion of Control, and Why Does It Exist?
Inversion of Control (IoC) means an object no longer creates
or looks up its own dependencies โ a container creates them and hands them in.
Dependency Injection (DI) is the specific technique Spring
uses to achieve that: dependencies arrive via constructor, setter, or field,
rather than the object instantiating them with new.
The problem this solves: when a class creates its own dependencies, it is permanently coupled to one concrete implementation. Swapping a real payment gateway for a test double means editing the class itself. Testing in isolation becomes impossible without a real database connection, a real HTTP client, a real everything. IoC inverts that: the class declares what it needs via its constructor signature, and remains ignorant of how that dependency gets built or which concrete implementation it actually receives.
// โ Without IoC โ UserService is permanently coupled to UserRepositoryImpl
public class UserService {
private final UserRepository userRepository;
public UserService() {
this.userRepository = new UserRepositoryImpl(); // hardcoded, untestable in isolation
}
}
// โ
With IoC โ Spring builds and hands in whatever implements UserRepository
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) { // no @Autowired needed โ single constructor (Spring 4.3+)
this.userRepository = userRepository;
}
}
// In tests: new UserService(mockRepository) โ no Spring container required at all
Since Spring 4.3, if a class has exactly one constructor, Spring uses it
for injection automatically โ @Autowired is redundant.
You only need @Autowired when a class declares
multiple constructors and you must tell Spring which one
to use. Writing it on every constructor regardless is outdated style.
Injection Types โ and Why Constructor Wins
// โ
Constructor injection โ the only one to use for required dependencies
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
public OrderService(OrderRepository orderRepository, PaymentService paymentService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
}
}
// โ ๏ธ Setter injection โ only for genuinely OPTIONAL dependencies
@Service
public class NotificationService {
private MetricsCollector metrics; // app works fine if this is never set
@Autowired(required = false)
public void setMetrics(MetricsCollector metrics) { this.metrics = metrics; }
}
// โ Field injection โ avoid in application code
@Service
public class BadExample {
@Autowired
private EmailService emailService; // can't be final, can't construct without reflection,
// dependency is invisible at the constructor
}
- Immutability โ fields can be
final, the object can never be in a half-constructed state - Fail-fast โ missing dependencies are a compile error, not a runtime NPE discovered in production
- True unit testing โ
new OrderService(mockRepo, mockPayment)works with zero Spring context, zero reflection, zero@MockBean - Surfaces design smells โ a constructor with 8 parameters is an honest, visible signal the class is doing too much. Field injection hides that signal indefinitely.
Declaring Beans
Stereotype annotations โ component scanning
@Component // generic โ anything not fitting a more specific stereotype
@Service // business logic layer โ semantically the same as @Component, signals intent
@Repository // data access layer โ ALSO enables automatic exception translation
// (JDBC/JPA exceptions become Spring's DataAccessException hierarchy)
@Controller // MVC presentation layer, returns view names
@RestController // = @Controller + @ResponseBody โ returns response bodies directly (REST APIs)
Explicit @Bean declarations โ for third-party classes you can't annotate
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.setConnectTimeout(Duration.ofSeconds(5)).build();
}
// Use @Bean specifically for classes you don't own โ can't add @Component to a
// third-party library's class. For your own classes, prefer stereotype annotations.
}
Resolving multiple implementations: @Primary and @Qualifier
public interface PaymentGateway { void charge(BigDecimal amount); }
@Service @Primary // default winner when multiple PaymentGateway beans exist and no qualifier is given
class StripeGateway implements PaymentGateway { ... }
@Service
class PaypalGateway implements PaymentGateway { ... }
@Service
public class CheckoutService {
private final PaymentGateway gateway;
public CheckoutService(@Qualifier("paypalGateway") PaymentGateway gateway) { // overrides @Primary explicitly
this.gateway = gateway;
}
}
Bean Lifecycle
A bean isn't just constructed once and forgotten โ Spring runs it through distinct phases, and you can hook into the boundaries.
/*
* 1. Constructor called (dependencies already resolved at this point)
* 2. Dependencies injected (setter/field injection happens here, if used)
* 3. @PostConstruct methods invoked โ bean is fully wired, ready for setup logic
* 4. Bean is in active use
* 5. @PreDestroy methods invoked โ container shutdown, last chance to clean up
*/
@Service
public class CacheWarmer {
private final ProductRepository repo;
private Map<Long, Product> cache;
public CacheWarmer(ProductRepository repo) { this.repo = repo; }
@PostConstruct
void warmUp() {
cache = repo.findAll().stream().collect(Collectors.toMap(Product::id, p -> p));
// runs once, after construction AND dependency injection โ repo is guaranteed non-null here
}
@PreDestroy
void shutdown() {
cache.clear(); // release resources before the container tears down
}
}
// Alternative for classes you can't annotate (third-party, via @Bean):
@Bean(initMethod = "start", destroyMethod = "stop")
public SomeThirdPartyClient client() { return new SomeThirdPartyClient(); }
Bean Scopes
| Scope | Lifetime | Use case |
|---|---|---|
singleton (default) |
One instance per container, shared everywhere | Stateless services โ the overwhelming majority of beans |
prototype |
New instance every time it's requested | Stateful, non-thread-safe objects |
request |
One instance per HTTP request | Per-request context data (web apps only) |
session |
One instance per HTTP session | User session state (web apps only) |
The scoped bean injection problem โ and how to actually fix it
A singleton is created once. If you inject a prototype or
request-scoped bean directly into a singleton's constructor, you
get exactly one instance of it, frozen forever โ defeating
the entire purpose of the narrower scope.
// โ BROKEN: TaskProcessor is prototype, but injected once into a singleton
// โ every call gets the SAME instance, not a fresh one
@Service // singleton by default
public class JobRunner {
private final TaskProcessor processor; // resolved ONCE, at JobRunner's own construction
public JobRunner(TaskProcessor processor) { this.processor = processor; }
}
// โ
FIX 1: ObjectProvider โ request a fresh instance on every call
@Service
public class JobRunner {
private final ObjectProvider<TaskProcessor> processorProvider;
public JobRunner(ObjectProvider<TaskProcessor> processorProvider) {
this.processorProvider = processorProvider;
}
public void runJob() {
TaskProcessor processor = processorProvider.getObject(); // fresh prototype instance, every call
processor.execute();
}
}
// โ
FIX 2: scoped proxy โ Spring injects a proxy that resolves the real
// instance from the current scope on every method call, transparently
@Component @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TaskProcessor { ... }
// Now direct constructor injection of TaskProcessor works correctly โ the
// injected reference is a proxy, not the real bean, and delegates per-call.
ApplicationContext vs BeanFactory
BeanFactory is the root IoC container interface โ lazy, minimal,
rarely used directly today. ApplicationContext extends it with
everything a real application needs: eager singleton instantiation by
default, internationalisation support, event publishing, and AOP
integration. In practice, you always work with
ApplicationContext; BeanFactory is mostly of
historical/interview interest.
// Spring Boot โ ApplicationContext created and managed automatically
@SpringBootApplication // = @Configuration + @EnableAutoConfiguration + @ComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// Without Spring Boot โ rare today, mostly for understanding what Boot does for you
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = ctx.getBean(UserService.class);
Publishing and listening to events
// Decouple "something happened" from "who reacts to it" โ beans don't
// need direct references to each other to communicate
public record OrderPlacedEvent(Long orderId, BigDecimal total) {}
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
public OrderService(ApplicationEventPublisher publisher) { this.publisher = publisher; }
public void placeOrder(Order order) {
// ... save order ...
publisher.publishEvent(new OrderPlacedEvent(order.id(), order.total()));
}
}
@Component
public class OrderNotifier {
@EventListener
void onOrderPlaced(OrderPlacedEvent event) {
// runs synchronously by default; add @Async to run on a separate thread
}
}
Circular Dependencies
@Service
public class A { public A(B b) { } }
@Service
public class B { public B(A a) { } } // A needs B, B needs A โ circular
Before Spring Boot 2.6, circular references between singletons were
resolved silently using early-reference proxies. Since 2.6, this
is disabled by default โ a circular dependency now throws
BeanCurrentlyInCreationException at startup instead of
being silently patched over. This breaks plenty of older codebases on
upgrade, and the fix is almost never to re-enable the old behaviour
(spring.main.allow-circular-references=true as a stopgap) โ
it's to actually redesign the dependency: extract the
shared logic into a third class both depend on, or use
@Lazy on one side to defer resolution. A circular dependency
is a design smell the framework is now correctly forcing you to confront
rather than quietly papering over.
Conditional Beans and Profiles
// Only register this bean when the active profile matches
@Service @Profile("prod")
class SesEmailService implements EmailService { ... }
@Service @Profile("!prod") // anything except prod โ dev, test, local
class ConsoleEmailService implements EmailService { ... }
// Register a bean only if a property has a specific value (or is absent + default given)
@Service
@ConditionalOnProperty(name = "feature.new-checkout", havingValue = "true")
class NewCheckoutFlow { ... }
Testing: Plain Unit Tests vs Spring Context Tests
// โ
FASTEST: plain unit test โ no Spring involved at all, just Java + Mockito
// This is exactly what constructor injection enables and field injection prevents
@Test
void createUser_sendsWelcomeEmail() {
UserRepository mockRepo = mock(UserRepository.class);
EmailService mockEmail = mock(EmailService.class);
UserService service = new UserService(mockRepo, mockEmail); // no Spring container, milliseconds to run
service.createUser(new User("a@b.com"));
verify(mockEmail).sendWelcomeEmail("a@b.com");
}
// โ ๏ธ SLOWER: @SpringBootTest loads the full ApplicationContext โ only for
// genuine integration tests that need real wiring, real config, real beans
@SpringBootTest
class UserServiceIntegrationTest {
@Autowired private UserService userService; // field injection is fine HERE โ test code, not production code
@MockitoBean // replaces the real bean in the context with a mock (Spring Boot 3.4+ name; @MockBean on older versions)
private EmailService emailService;
}
A @SpringBootTest loads the entire context โ every bean,
every auto-configuration, every property source. On a real application
this can take seconds per test class, multiplied across hundreds of
classes in CI. Reach for it only when you genuinely need Spring's wiring
under test (a controller's request mapping, a repository against a real
database via Testcontainers). For pure business logic, constructor
injection means you never need Spring running at all.
Interview Questions
Q: What is the difference between IoC and DI?
IoC is the principle โ control over object creation is inverted from the
application to a container. DI is the technique Spring uses to implement
it โ dependencies are injected via constructor, setter, or field rather than
created internally with new.
Q: What is a Spring bean?
Any object whose lifecycle (creation, configuration, dependency wiring,
destruction) is managed by the Spring IoC container, rather than by
application code calling new directly.
Q: Why is constructor injection preferred over field injection?
Constructor injection allows final fields (true immutability),
makes missing dependencies a compile-time error instead of a runtime NPE,
and allows the class to be instantiated and unit-tested with plain
new, with no Spring container required at all.
Q: What is the scoped bean injection problem, and how do you fix it correctly?
Injecting a narrower-scoped bean (prototype, request)
directly into a singleton resolves it exactly once, at the singleton's own
construction โ every subsequent use sees the same frozen instance,
defeating the scope entirely. Fix with ObjectProvider<T>
(explicitly request a fresh instance per call) or a scoped proxy
(proxyMode = ScopedProxyMode.TARGET_CLASS, which makes the
injected reference a proxy that re-resolves the real bean from the active
scope on every method invocation, transparently).
Q: Why did Spring Boot 2.6 change how it handles circular dependencies, and what's the right fix?
Before 2.6, circular references between singletons were silently resolved
using early-reference proxies during bean creation. Since 2.6, this is
disabled by default โ Spring now throws
BeanCurrentlyInCreationException at startup, treating a
circular dependency as the design problem it actually is rather than quietly
working around it. The correct fix is virtually never re-enabling the legacy
flag โ it's extracting the shared responsibility into a third class both
sides depend on, breaking the cycle structurally.
Q: Why does @Repository do more than just mark a bean for component scanning?
@Repository additionally enables Spring's
PersistenceExceptionTranslationPostProcessor, which wraps the
bean's methods to translate platform-specific exceptions (JDBC
SQLException, JPA's PersistenceException) into
Spring's unified, unchecked DataAccessException hierarchy. This
means calling code can catch a single consistent exception type regardless of
which persistence technology sits underneath โ a detail
@Component alone would not provide.
Q: When would you deliberately use @SpringBootTest over a plain unit test?
Only when the test's purpose is verifying Spring's own wiring behaviour โ
that a controller correctly maps a request, that auto-configuration produces
the expected beans, that a repository works against a real database via
Testcontainers. For testing business logic itself, a plain unit test with
manually constructed mocks is faster, simpler, and doesn't conflate "is my
logic correct" with "is my Spring configuration correct" โ two genuinely
different concerns that a full context load tests simultaneously and slowly.