Spring Core Fundamentals - Complete Notes
A complete mental model of Spring's IoC container, dependency injection, beans, configuration, scopes, lifecycle, component scanning, dependency selection, extension points, and common interview traps.
00. The Spring Core mental model
Spring Core is an object factory plus an object graph assembler. Your classes describe what they need, configuration describes which objects exist, and the container creates and connects them.
In plain Java, an object often creates or locates its collaborators. A service might call
new PostgresOrderRepository() directly. That service now controls both business
behavior and infrastructure construction. In Spring, the service declares an
OrderRepository dependency, and the container supplies a configured implementation.
The business class stays focused on business behavior.
Think of a restaurant kitchen. A chef declares the station and ingredients needed for a dish. The kitchen manager assigns a prepared station and supplies ingredients. The chef does not build an oven, negotiate with suppliers, or search every cupboard during each order. Spring plays the kitchen-manager role for application objects.
| Term | Meaning | Question it answers |
|---|---|---|
| IoC | Framework controls creation and wiring | Who assembles the application? |
| DI | Dependencies are supplied from outside | How does an object receive collaborators? |
| Bean | An object managed by the container | Which objects receive Spring services? |
| Bean definition | Metadata used to create a bean | How should the bean be constructed? |
| ApplicationContext | The commonly used IoC container API | Where is the managed object graph held? |
Dependency injection magnifies the design you give it. Small classes with clear responsibilities
become easy to assemble and test. A large class with hidden state remains difficult even after
adding @Component.
01. Inversion of Control and dependency injection
IoC is the broad principle. Dependency injection is Spring's primary technique for implementing it.
Control is inverted because application objects no longer control the construction or lookup of their dependencies. They expose constructor parameters, factory-method parameters, or properties. The container resolves those requirements from its bean definitions.
public final class CheckoutService {
private final PaymentGateway gateway =
new StripePaymentGateway(System.getenv("STRIPE_KEY"));
public Receipt checkout(Order order) {
return gateway.charge(order);
}
}
public final class CheckoutService {
private final PaymentGateway gateway;
public CheckoutService(PaymentGateway gateway) {
this.gateway = gateway;
}
public Receipt checkout(Order order) {
return gateway.charge(order);
}
}
The second class is ordinary Java. It does not import Spring. Production configuration can supply a real gateway, while a unit test can supply a fake. This is dependency inversion as well as dependency injection: high-level policy depends on an abstraction rather than a concrete remote client.
Constructor, setter, and field injection
| Style | Best use | Main tradeoff |
|---|---|---|
| Constructor | Required dependencies | Dependencies are explicit and object can be immutable |
| Setter | Truly optional or reconfigurable dependency | Object may exist temporarily in an incomplete state |
| Field | Rare framework integration cases | Hidden dependency, reflection required, weak testability |
The compiler forces callers to provide every required collaborator. Fields can be final. Tests instantiate the class without starting Spring. A constructor with too many parameters also exposes a class that probably has too many responsibilities.
02. BeanFactory and ApplicationContext
BeanFactory defines the basic container contract.
ApplicationContext builds on it with the features most applications need.
ApplicationContext creates, configures, and assembles beans from configuration
metadata. It also integrates event publication, internationalization, resource loading,
environment and profile support, annotation processing, and AOP infrastructure. Application code
normally lets Spring Boot create the context instead of constructing it manually.
try (var context =
new AnnotationConfigApplicationContext(AppConfiguration.class)) {
CheckoutService service = context.getBean(CheckoutService.class);
service.checkout(sampleOrder());
}
Calling applicationContext.getBean(...) throughout business code hides dependencies
and couples the code to Spring. Prefer constructor injection. Programmatic lookup is appropriate
mainly in framework integration, bootstrapping, or genuinely dynamic selection.
Sources of configuration metadata
- Annotated component classes discovered through classpath scanning.
@Configurationclasses containing@Beanfactory methods.- XML bean definitions, still supported for legacy and special integration needs.
- Programmatic bean registration for frameworks and dynamic infrastructure.
03. Bean definitions and bean identity
A bean definition is a recipe. The bean is the object created from that recipe.
Definition metadata can include the bean class or factory method, constructor arguments, properties, scope, lazy behavior, initialization and destruction callbacks, qualifiers, and dependencies. A singleton definition normally produces one object per container, not one object per JVM.
@Configuration
class BillingConfiguration {
@Bean
PaymentGateway paymentGateway(BillingProperties properties) {
return new StripePaymentGateway(properties.apiKey());
}
@Bean
CheckoutService checkoutService(PaymentGateway gateway) {
return new CheckoutService(gateway);
}
}
By default, a @Bean method name becomes the bean name. Component scanning derives a
name such as checkoutService from CheckoutService. Prefer dependency
resolution by type. Use names and qualifiers only when the domain genuinely has multiple
candidates.
FactoryBean is not an ordinary factory bean
A class implementing FactoryBean<T> is managed by Spring, but a normal lookup
returns the product T. Prefixing the bean name with & asks for the
factory object itself. Frameworks use this extension point to build objects that need complex
creation logic, such as proxies.
04. Stereotypes and component scanning
Component scanning finds candidate classes and registers bean definitions. It does not scan the entire JVM automatically.
| Annotation | Intent | Extra meaning |
|---|---|---|
@Component |
General managed component | Base stereotype |
@Service |
Application or domain service | Communicates architectural role |
@Repository |
Persistence component | Participates in exception translation |
@Controller |
Spring MVC controller | Web request handling semantics |
@Configuration |
Source of bean definitions | Declares configuration intent |
@Repository
final class JdbcOrderRepository implements OrderRepository {
// persistence implementation
}
@Service
final class OrderService {
private final OrderRepository orders;
OrderService(OrderRepository orders) {
this.orders = orders;
}
}
A single constructor does not need @Autowired. With several constructors, mark the
intended injectable constructor or make the choice unambiguous. Keep the main application class in
a root package so its default scan covers subpackages without accidentally scanning unrelated
code.
Package every class deliberately. Broad scans can pull test fixtures, optional adapters, or duplicate implementations into the context. Use explicit imports or narrow scan boundaries for modular applications.
05. Java configuration and @Bean methods
Component scanning is convenient for classes you own. @Bean is ideal for third-party
classes and explicit infrastructure assembly.
@Configuration(proxyBeanMethods = false)
class ClientConfiguration {
@Bean
FraudClient fraudClient(FraudProperties properties) {
return FraudClient.builder()
.baseUrl(properties.baseUrl())
.connectTimeout(properties.connectTimeout())
.build();
}
}
With proxyBeanMethods = false, direct Java calls between @Bean methods
are ordinary calls and may create extra objects. Prefer method-parameter injection as shown above.
Full configuration proxying can intercept calls between bean methods to preserve container
semantics, but explicit parameter injection is clearer and avoids unnecessary proxying.
@Import and modular configuration
@Import composes known configuration classes without expanding a component scan. This
supports feature modules with explicit boundaries. A module can publish one configuration class
while keeping internal implementation packages unscanned by consumers.
06. How dependency resolution works
Spring primarily resolves injection points by assignable type, then uses qualifiers and priority metadata to disambiguate candidates.
interface MessageSender {
void send(Message message);
}
@Component
class EmailSender implements MessageSender { /* ... */ }
@Component
class SmsSender implements MessageSender { /* ... */ }
Injecting one MessageSender now fails with a
NoUniqueBeanDefinitionException. Spring should not guess which business channel is
correct.
@Primary
@Primary marks a default candidate when several beans match one value injection
point. It is useful when one implementation is the application-wide default, but it should not
hide domain decisions that belong in explicit orchestration.
@Qualifier
@Component
@Qualifier("transactional")
class TransactionalEmailSender implements MessageSender { /* ... */ }
@Service
class ReceiptService {
ReceiptService(@Qualifier("transactional") MessageSender sender) {
this.sender = sender;
}
}
A qualifier narrows candidates; it is not merely a bean-name lookup. Custom qualifier annotations
make intent type-safe and reusable. For example, @TransactionalChannel is clearer
than repeating a string across the codebase.
Injecting all implementations
Inject List<Validator>, Set<Handler>, or
Map<String, Handler> when the application intentionally composes every matching
strategy. Use @Order or Ordered when iteration order has meaning.
07. Optional, lazy, and provider dependencies
Different injection forms express different lifecycle and availability requirements.
| Form | Meaning | Use carefully because |
|---|---|---|
Optional<T> |
Zero or one candidate | Optional infrastructure can spread branching logic |
ObjectProvider<T> |
Lazy or repeated programmatic retrieval | It introduces container-aware lookup |
@Lazy T |
Inject a proxy and resolve target later | Failures move from startup to first use |
List<T> |
All matching candidates | Every discovered bean joins the behavior |
Default eager singleton creation makes startup a useful validation phase. The application fails early when wiring is invalid. Use laziness for measured startup or lifecycle reasons, not to hide broken dependency graphs.
08. Bean scopes
Scope answers how long a bean instance lives and which callers share it.
| Scope | Instance boundary | Typical use |
|---|---|---|
| singleton | One per bean definition per container | Stateless services and shared infrastructure |
| prototype | New instance for each container retrieval | Stateful helper that Spring creates |
| request | One per HTTP request | Request-specific context |
| session | One per HTTP session | Session state, used sparingly |
| application | One per ServletContext | Web-application shared state |
| websocket | One per WebSocket session | WebSocket conversation state |
A singleton service is shared by concurrent requests. Prefer immutable configuration and local variables. Mutable request data in fields creates races. Spring controls lifetime, not the correctness of concurrent access.
Shorter-lived dependency inside a singleton
Injecting a prototype directly into a singleton resolves the prototype only while the singleton is
created. It does not create a new prototype on every method call. Use
ObjectProvider, method injection, or a scoped proxy when repeated contextual lookup
is truly required.
09. Bean lifecycle
Bean creation is a pipeline. Understanding the pipeline explains why proxies, annotation processing, and lifecycle callbacks behave as they do.
- Load and register bean definitions.
- Run bean-factory post-processors that may change definitions.
- Instantiate the bean through a constructor or factory method.
- Populate dependencies and aware callbacks.
- Run bean post-processors before initialization.
- Run initialization callbacks.
- Run bean post-processors after initialization, possibly returning a proxy.
- Expose the ready bean for use.
- On context shutdown, run destruction callbacks for eligible scopes.
@Component
final class PricingCache {
@PostConstruct
void load() {
// validate configuration or warm bounded state
}
@PreDestroy
void close() {
// release owned resources
}
}
Alternatives include InitializingBean, DisposableBean, and
@Bean(initMethod = "...", destroyMethod = "..."). Annotations or explicit bean
methods avoid coupling domain classes to Spring interfaces.
Spring creates and configures prototype objects but does not automatically manage their complete destruction lifecycle. The caller holding the object must release resources. Avoid placing expensive owned resources inside prototype beans without a clear cleanup design.
10. Container extension points
Post-processors let infrastructure alter definitions or wrap objects without changing business classes.
BeanFactoryPostProcessor
A bean-factory post-processor works on bean definitions before normal beans are instantiated. Property placeholder resolution and configuration-class processing use this phase. It should not eagerly fetch ordinary beans because doing so can bypass later processing.
BeanPostProcessor
A bean post-processor sees bean instances around initialization. Annotation support and many auto-proxy mechanisms build on this extension point. The object returned after processing may be a proxy rather than the original instance.
@Autowired, lifecycle annotations, transactions, caching, security, and async
behavior are not magic keywords interpreted by Java. Registered infrastructure examines bean
metadata and often creates proxies around eligible managed objects.
11. Proxies and the managed-bean boundary
Spring can return a proxy that implements cross-cutting behavior around method calls.
A proxy stands between the caller and target. It can start a transaction, check authorization, consult a cache, record metrics, or schedule asynchronous work before delegating. Calls must pass through the proxy for proxy advice to run.
@Service
class ImportService {
void importAll() {
importOne(); // direct call on this, bypasses proxy interception
}
@Transactional
public void importOne() {
// expected transaction advice may not run on self-invocation
}
}
The clean fix is usually to move the advised operation to another focused bean and inject it. Self-injecting a proxy makes the class harder to reason about and often signals mixed responsibilities.
12. Circular dependencies
A cycle means A needs B while B needs A. Constructor injection exposes the cycle immediately.
@Service
class OrderService {
OrderService(InventoryService inventory) { /* ... */ }
}
@Service
class InventoryService {
InventoryService(OrderService orders) { /* ... */ }
}
The container cannot construct either object first. Setter injection or laziness can defer some cycles, but that treats the symptom. Better fixes include extracting shared policy, introducing a one-way port, publishing an event for a secondary reaction, or combining responsibilities that were incorrectly split.
Do not enable circular references globally just to make startup pass. Ask which dependency direction represents the real business flow.
13. Environment, properties, and profiles
Spring Core exposes an Environment containing profiles and property sources.
@Profile conditionally registers a component or configuration for selected
environments. Profiles are useful for coarse environment-specific object graphs, such as a local
fake adapter versus a production integration. They should not become a substitute for ordinary
typed configuration.
@Configuration
@Profile("local")
class LocalPaymentConfiguration {
@Bean
PaymentGateway paymentGateway() {
return new FakePaymentGateway();
}
}
Keep secrets outside source control. Validate required values at startup. Prefer structured
configuration binding in Spring Boot for groups of related properties instead of scattering
string-based @Value expressions through business code.
14. Events, resources, and messages
ApplicationContext provides useful infrastructure beyond object construction.
Application events
A publisher can emit a domain-relevant application event without knowing every in-process listener. Events reduce direct coupling, but ordinary listeners run synchronously by default. They are not automatically durable, retried, or delivered across services.
Resource abstraction
Spring's Resource provides a common API for classpath files, filesystem files, URLs,
and other resource locations. Code can consume an InputStream without hard-coding
where the resource lives.
MessageSource
MessageSource resolves messages by code and locale. It supports internationalized
user-facing text. Do not use localization bundles as a general configuration database.
15. Testing Spring-managed designs
Most business classes should be testable without a Spring context.
@Test
void declinesWhenGatewayRejectsPayment() {
PaymentGateway gateway = order -> PaymentResult.declined("limit");
CheckoutService service = new CheckoutService(gateway);
Receipt receipt = service.checkout(sampleOrder());
assertThat(receipt.status()).isEqualTo(DECLINED);
}
Start an ApplicationContext when the test is specifically about configuration,
scanning, profiles, proxies, or integration. A context test is slower and can hide unclear class
design behind framework setup.
16. Common mistakes and corrections
| Mistake | Why it hurts | Correction |
|---|---|---|
| Field injection everywhere | Dependencies are hidden and fields cannot be final | Use constructor injection |
| Mutable data in singleton fields | Concurrent requests race | Keep services stateless or synchronize deliberately |
Calling getBean in business logic |
Creates service-locator coupling | Inject the required abstraction |
Using @Primary to hide domain choices |
Wrong implementation can be selected silently | Use semantic qualifiers or explicit orchestration |
| Making every object a bean | Domain values gain unnecessary container lifecycle | Manage services and infrastructure, create values normally |
Using @Lazy to repair cycles |
Moves design failure to runtime | Break the dependency cycle |
| Expecting proxy advice on self-calls | Call never crosses the proxy | Move the boundary to another bean |
| Broad component scanning | Unexpected beans and collisions appear | Use clear package and module boundaries |
17. Production design guidelines
- Keep singleton services stateless unless shared state is explicitly synchronized.
- Inject interfaces at boundaries where implementations genuinely vary.
- Do not create an interface for every class without a substitution or boundary reason.
- Use constructor injection for required collaborators and validate configuration at startup.
- Keep configuration near the module it assembles, with explicit public entry points.
- Prefer one direction of dependency flow and treat cycles as design defects.
- Keep domain values and entities as ordinary objects unless container services are required.
- Know when a returned bean is a proxy and which calls cross that proxy.
- Use eager startup validation by default, then optimize measured startup bottlenecks.
- Shut contexts down gracefully so destruction callbacks can release owned resources.
18. Debugging container failures
| Exception or symptom | Likely cause | First check |
|---|---|---|
NoSuchBeanDefinitionException |
No candidate was registered | Scan boundary, profile, condition, and configuration import |
NoUniqueBeanDefinitionException |
Several candidates match | Use a qualifier, primary, or explicit collection |
BeanCurrentlyInCreationException |
Circular dependency or early lookup | Draw the constructor dependency graph |
| Annotation appears ignored | Object was not created by Spring or call bypassed proxy | Find who constructed the object and who invoked the method |
| Wrong implementation injected | Broad primary or qualifier mismatch | List candidate definitions and injection metadata |
19. Interview questions and answers
What is the difference between IoC and DI?
IoC is the broad principle that control of creation and flow moves from application objects to a framework or container. DI is a concrete IoC technique where an object's dependencies are supplied through constructors, factory methods, or properties.
What is a Spring bean?
It is an object whose creation, configuration, assembly, and lifecycle are managed by a Spring IoC container. An ordinary object does not become a bean merely because its class has a familiar name.
BeanFactory versus ApplicationContext?
BeanFactory defines basic bean-container behavior. ApplicationContext
extends it with application-oriented services such as events, messages, resources, environment
support, and convenient integration with post-processors and AOP.
Why prefer constructor injection?
Required dependencies are explicit, fields can be final, invalid objects cannot be created accidentally, tests require no reflection or container, and excessive dependencies reveal responsibility problems.
Is a Spring singleton global?
No. It is normally one instance per bean definition per IoC container. Multiple contexts can each have their own instance. It also has no automatic thread-safety guarantee.
What happens when two beans implement the same interface?
A single-value injection point is ambiguous unless selection metadata resolves it. Use
@Qualifier for semantic selection, @Primary for a true default, or
inject a collection when all implementations are intended.
Why can self-invocation bypass @Transactional?
Proxy-based advice runs when a caller invokes the proxy. A method calling another method on
this makes a direct target call and never crosses the proxy boundary.
Does prototype scope mean a new object on every method call?
No. It means a new object for each container retrieval or resolution. If one prototype is injected into a singleton during construction, that singleton keeps the same injected instance.
How should circular dependencies be fixed?
Identify the incorrect dependency direction. Extract shared policy, introduce a port, move an operation to a focused service, or use an event for a secondary reaction. Laziness and setters can hide the cycle but rarely improve the design.
When should @Bean be preferred over @Component?
Use @Bean when configuring a third-party class, when construction needs explicit
factory logic, or when a module should expose deliberate configuration. Use scanned components for
application classes you own when scanning makes the module clearer.
20. Practice exercises
- Refactor a service that constructs a repository into constructor-injected ports and adapters.
- Create two notification senders and select one with a custom qualifier annotation.
- Inject all validators as an ordered list and explain who controls ordering.
- Demonstrate why a prototype injected directly into a singleton is reused.
- Write a context test that proves a profile selects a fake adapter.
- Draw the lifecycle stages where bean-factory and bean post-processors run.
- Break a circular dependency without using setter injection or
@Lazy. - Reproduce a self-invocation proxy failure and fix it by extracting a bean.
21. One-screen cheat sheet
- IoC decides who controls assembly; DI decides how collaborators arrive.
- A bean is an object managed by an
ApplicationContext. - Use constructor injection for required dependencies.
- Resolve by type, qualify deliberate alternatives, and use primary only for a true default.
- Singleton means one per definition per container, not thread-safe and not JVM-global.
- Prototype means new per retrieval, not new per method call.
- Use
@Beanfor explicit or third-party construction. - Post-processors power much of Spring's annotation and proxy behavior.
- Proxy advice requires a call through the proxy.
- Constructor cycles usually reveal an architectural problem.
- Unit-test business objects without Spring whenever possible.