This content originally appeared on DEV Community and was authored by kouta222
Introduction
The Spring Framework has transformed Java enterprise development with its lightweight, non-invasive design. It emphasizes simplicity, testability, and maintainability through Inversion of Control (IoC), Dependency Injection (DI), and Aspect-Oriented Programming (AOP). This article keeps your original volume, adds precise corrections, and expands explanations so a motivated beginner-to-intermediate reader can build accurate intuition and avoid common traps in production.
Core Philosophy
Lightweight & Non-Invasive
Write POJOs that don’t extend framework base classes. Spring integrates via metadata (annotations/config), not inheritance.
Separation of Concerns
Distinguish domain logic (services), infrastructure (repositories, messaging), and cross‑cutting concerns (transactions, security, caching via AOP).
Convention over Configuration
Spring Boot supplies sane defaults and auto-configuration; you override only what you need.
Testability First
DI allows swapping fakes/mocks, minimizing static/global state and making unit/integration tests straightforward.
Mental model: The container orchestrates object graphs and lifecycle; your code expresses intent (interfaces, annotations, configs).
IoC & Dependency Injection — What It Is and Isn’t
IoC means the container owns creation, wiring, and lifecycle. You declare what you need; the container decides when/how to provide it.
It is:
- Constructor/setter/field injection performed by the container
- Externalized configuration and lifecycle callbacks
- Event-driven control flow (framework calls you)
It is not:
- Service Locator patterns that call back into the container everywhere
- Heavy factories sprinkled across your code
- Static singletons and manual wiring
What the Container Actually Does
- Bean definition loading (from annotations, @Configuration, XML)
- Dependency resolution (by type → qualifiers → names → @primary)
- Instantiation (constructors/factory methods/FactoryBean)
- Decoration (apply BeanPostProcessors, create AOP proxies if needed)
- Scope & lifecycle (singleton/prototype/web scopes; destruction callbacks)
Deep Dive: Container Startup (Step‑by‑Step)
Before we break down the sequence, remember that the ApplicationContext is more than just a bean factory. It’s the supervisor of your workshop — managing blueprints, coordinating schedules, enforcing safety rules, and making sure every tool (bean) is ready to work together.
It extends the simpler BeanFactory (which only handles the core DI mechanics) and layers on practical capabilities: a configuration model that understands @Configuration, @bean, component scanning, XML, and imports; lifecycle orchestration via the refresh() workflow; environment and profile management for property sources and @profile filtering; an event bus (ApplicationEventPublisher) for publish/subscribe; internationalization through MessageSource; resource loading from the classpath, URLs, or the filesystem; type conversion and validation through a ConversionService; integration with AOP and autowiring hooks via BeanPostProcessors; and the ability to form parent/child contexts for modular composition.
When your application starts, Spring takes the ApplicationContext through a well-defined refresh lifecycle. Let’s walk through it as if we’re setting up a brand-new workshop from scratch.
1. Create context instance
Spring builds the “box” that will hold all your beans.
When you start your app (SpringApplication.run(...)
or create an ApplicationContext manually), Spring first creates the container object — the thing responsible for managing all your beans.
Think of it like opening an empty toolbox that’s about to be filled with tools (your beans).
2. Load configuration
Find out what beans should exist.
Spring now reads your app’s configuration:
-
Property sources —
.properties
or.yml
files, environment variables, system properties. - @Configuration classes — special classes with @bean methods.
- Component scanning — looks through packages for @Component, @Service, @Controller, etc., and makes “bean blueprints” for them.
At this stage, no beans are created yet — Spring is just making a shopping list of what to build later.
3. Register infrastructure post-processors
Add special helpers that know how to handle Spring’s own features.
Some beans aren’t your beans — they’re Spring’s own support beans.
For example:
-
ConfigurationClassPostProcessor
— understands @Configuration and @bean. - Others handle @Enable… annotations, validation, etc.
You can think of these as setup crew members who prepare the rules before any actual objects are made.
4. Invoke processors that adjust bean definitions
Change the blueprints before building anything.
Spring calls BeanFactoryPostProcessors:
- These can add, remove, or modify bean definitions.
- Example: PropertySourcesPlaceholderConfigurer replaces
${my.value}
with the actual number from your properties file. - Also where @profile filtering happens — beans not matching the active profile are removed before they’re built.
Analogy: before construction starts, the architect might update the blueprints with the latest measurements.
5. Register instance-level processors
Hook into beans after they’re built but before you use them.
These are BeanPostProcessors:
- Do autowiring (@Autowired, @Value).
- Call lifecycle annotations (@PostConstruct).
- Create AOP proxies (wrapping beans in decorators for transactions, logging, etc.).
Think of these as quality inspectors — every finished bean passes through them before entering the container.
6. Instantiate non-lazy singletons
Build the actual objects, in the right order.
Spring now builds all non-lazy singleton beans:
- It looks at dependencies to know who to create first.
- @DependsOn can force certain beans to be created before others.
- If there’s a circular dependency (Bean A needs Bean B and vice versa), Spring uses an internal trick (early references) to handle it for singletons.
This is the factory floor — beans go from blueprints to real objects here.
7. Initialization & proxying
Final touches before the beans are “ready for action.”
For each bean:
- postProcessBeforeInitialization — BeanPostProcessors can tweak it before init.
- Init methods — your @PostConstruct or InitializingBean.afterPropertiesSet().
- postProcessAfterInitialization — last chance for wrapping/proxying (AOP).
Example: AnnotationAwareAspectJAutoProxyCreator might wrap your service in a proxy that handles @Transactional.
This is like polishing and adding safety features before putting the tool into the toolbox.
8. Ready: context is refresh-complete
The app is now “live.”
- Spring publishes events like ContextRefreshedEvent.
- Beans that implement ApplicationListener can react.
- Beans implementing SmartLifecycle can start background jobs.
- In Spring Boot, CommandLineRunner and ApplicationRunner beans run here.
Your toolbox is full, organized, and ready for you to start working.
Ordering semantics: PriorityOrdered/Ordered affect the order of post-processors and certain callback beans, not the order regular application beans are created.
Wiring Rules That Bite in Practice
- Constructor injection is preferred (immutability, fail-fast for missing deps).
- If multiple beans of the same type exist:
- Use @primary to pick a default.
- Use @Qualifier at the injection point for explicit choice.
- Collections (e.g.,
List<Foo>
) are ordered using @Order/Ordered for the injected collection only.
Scopes (and Scoped Proxies) with Realistic Patterns
- Singleton (default): one instance per container.
-
Prototype: a new instance per lookup. Injecting a prototype into a singleton gives you one snapshot at injection time. For true per‑use behavior, inject
ObjectProvider<PrototypeType>
and callgetObject()
when needed. - Web scopes: @RequestScope, @SessionScope.
AOP Essentials — What Proxies Can and Can’t Do
Spring AOP applies advice via proxies:
- JDK dynamic proxies if the target implements at least one interface.
- CGLIB class-based proxies otherwise. You can force class proxies with
spring.aop.proxy-target-class=true
.
Limitations to remember:
- Only public methods are reliably proxied by default in proxy-based AOP.
- Calls made within the same class (self-invocation) bypass the proxy; advice won’t run.
- final/private methods cannot be advised with proxy-based AOP.
Self-invocation solutions:
- Extract the advised method to another bean and inject it.
- Self-inject the proxied bean (works, but be explicit why).
- Use
AopContext.currentProxy()
with@EnableAspectJAutoProxy(exposeProxy = true)
as an advanced option.
Transactions — Practical Fundamentals
- Annotate public methods on Spring-managed beans with @Transactional.
-
Read-only:
@Transactional(readOnly = true)
hints for optimizations; it does not block writes by itself. - Transaction manager: provided by data starters in Boot; otherwise register one and (if multiple) select with qualifiers.
- Placement: Avoid annotating private/final methods; ensure external calls go through the proxy boundary.
Caching — Keys, Eviction, and Pitfalls
- Enable with @EnableCaching.
- Use
@Cacheable(cacheNames = "products", key = "#id")
, @CacheEvict, @CachePut. - Key evaluation uses SpEL; make keys explicit to avoid surprises.
- Backing store decides TTL/size (e.g., Caffeine, Redis). Configure at the cache manager level.
- Same self-invocation caveat: cache advice won’t apply to internal calls.
Retry — Transient Fault Handling
- Add Spring Retry dependency and @EnableRetry.
- Annotate with
@Retryable(value = IOException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2.0))
. - Provide a @Recover method with a matching signature for fallback after retries.
- Be mindful of idempotency; don’t blindly retry non-idempotent operations.
Method-Level Security — Expressions in Practice
- Enable with @EnableMethodSecurity.
- Use @PreAuthorize/@PostAuthorize with SpEL (e.g.,
@PreAuthorize("hasRole('ADMIN')")
, orhasPermission(#id, 'User', 'READ')
). - Security advice is AOP: same proxy limitations apply. Keep methods public, and don’t expect internal calls to be secured unless going through the proxy.
Conditional & Profile-Driven Configuration
Content appears to be incomplete in the original document
Lifecycle & Shutdown — Beyond @PostConstruct
- Awareness interfaces (BeanNameAware, BeanFactoryAware, etc.) can provide context but use sparingly.
- SmartLifecycle supports phased startup/shutdown; useful for coordinating resources (messaging consumers, schedulers).
- Application events (ContextRefreshedEvent, ContextClosedEvent) let you hook into startup/shutdown safely.
Tightening Corrections (Why They Matter)
- @Order vs creation order: Prevents incorrect assumptions; @Order affects injection order for collections and certain callbacks, not bean creation timing. Use @DependsOn or proper dependency wiring to control creation.
- @Service on interfaces: Avoids “bean not found” at runtime. Component scanning registers concrete classes, not plain interfaces.
- Interface-only proxies: The Proxy.newProxyInstance example works only when the bean type is an interface. For concrete classes, rely on CGLIB or Spring AOP.
- Feature toggles: Annotations like @Cacheable, @Retryable, @PreAuthorize, and @Transactional often require enabling annotations (e.g., @EnableCaching). Without them, the annotations silently do nothing.
- Request-scoped controllers: These are atypical; controllers should usually be singletons. Inject request-scoped collaborators via proxies instead.
- FactoryBean: Fine for demonstrations, but in production prefer a pooled DataSource and obtain a Connection per operation.
- Boot-specific conditions: @ConditionalOnProperty and similar annotations live in Spring Boot and assume Boot’s auto-configuration machinery is present on the classpath.
Minimal, Correct Code Snippets
Enabling Features (Spring Boot)
@SpringBootApplication
@EnableCaching
@EnableRetry
@EnableMethodSecurity
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Description: This configures a Spring Boot application with caching, retry logic, and method-level security enabled. In non-Boot apps, add @EnableTransactionManagement to enable @Transactional support.
Bean Creation Order
@Configuration
class OrderedConfig {
@Bean
@DependsOn({"dataSource", "transactionManager"})
public ApplicationService applicationService() {
return new ApplicationService();
}
@Bean
public DataSource dataSource() {
return new DataSource();
}
@Bean
public TransactionManager transactionManager() {
return new TransactionManager();
}
}
Description: This setup ensures ApplicationService is only created after both dataSource and transactionManager beans are available. Note that @Order on bean methods does not affect creation timing.
Self-invocation (One of the Safe Patterns)
@EnableAspectJAutoProxy(exposeProxy = true)
@Service
public class UserService {
@Transactional
public void updateUser(User user) {
// Business logic for updating a user
}
public void doSomething(User user) {
((UserService) AopContext.currentProxy()).updateUser(user);
}
}
Description: Demonstrates handling the self-invocation problem. By exposing the proxy, internal method calls still pass through transactional advice. A more maintainable alternative is to move updateUser into another bean.
Testing With the Container in Mind
- Full context: @SpringBootTest for end-to-end wiring and AOP/transactions.
- Mocking collaborators: @MockBean to replace a bean in the context.
- Profile-specific tests: use @ActiveProfiles(“test”) with test configurations.
Conclusion
By grounding your understanding in how the container really starts, wires, and decorates beans-and by recognizing proxy limitations and feature-enabling requirements-you can translate Spring’s philosophy into production-grade code. Keep aspects focused, dependencies explicit, and configuration type-safe. With these patterns, your original structure (philosophy → IoC/DI → container → lifecycle → AOP → real patterns) becomes a reliable mental map you can reuse across services.
References
- Spring Framework Reference Documentation
- Advanced Spring for Professionals — Masatoshi Tada
- Inversion of Control | Baeldung
This content originally appeared on DEV Community and was authored by kouta222