Spring Core
Every enterprise Java service you ship in 2026 runs on Spring — not because it is trendy, but because Inversion of Control (IoC), Dependency Injection (DI), and a well-defined b…
Introduction
Every enterprise Java service you ship in 2026 runs on Spring — not because it is trendy, but because Inversion of Control (IoC), Dependency Injection (DI), and a well-defined bean lifecycle turn tangled constructor graphs into testable, composable modules. When a payment service needs a fraud checker, ledger repository, and metrics publisher, Spring's ApplicationContext wires them once at startup — not via new FraudService(new HttpClient(...)) scattered across 200 classes.
This lesson covers Spring Core at staff-engineer depth: how the IoC container creates and manages beans, constructor vs field injection trade-offs, the full bean lifecycle from instantiation through destruction, and Aspect-Oriented Programming (AOP) for cross-cutting concerns like auditing, retries, and transaction boundaries — without polluting business logic.
Spring Core is the foundation beneath Spring Boot auto-configuration. You cannot debug "bean not found," circular dependency failures, or proxy-not-applied bugs until you understand what the container actually does.
Business problem
Teams without Spring Core literacy ship fragile services:
- God classes: Manual
newwiring creates 800-line constructors and untestable static singletons. - Circular dependency crashes: Service A needs B, B needs A — startup fails at 3 AM deploy with obscure stack trace.
- AOP surprises:
@Transactionalor@Cacheable"doesn't work" because method called viathis— bypasses proxy. - Memory leaks: Prototype beans injected into singletons, or listeners never removed — Metaspace and heap grow over weeks.
- Test brittleness: Integration tests spin full context when unit tests with
@MockBeanwould suffice — CI times explode.
Why this topic exists
Spring Core solves the enterprise wiring problem that every large Java codebase faces:
- Decouple construction from use: Components declare dependencies; container resolves graph at startup.
- Single responsibility: Business code focuses on domain rules; infrastructure (DB, HTTP, metrics) injected.
- Cross-cutting without duplication: AOP applies logging, security, transactions declaratively.
- Testability: Replace real beans with mocks/stubs via
@TestConfiguration— same interfaces, different implementations. - Lifecycle hooks:
@PostConstruct,InitializingBean,@PreDestroy— predictable init/shutdown for connection pools and caches.
Core concepts
Four pillars of Spring Core:
- IoC (Inversion of Control): Framework controls object creation and dependency graph — you declare beans, container manages lifecycle.
- DI (Dependency Injection): Dependencies supplied via constructor (preferred), setter, or field — constructor injection makes required deps explicit and enables immutability.
- Bean lifecycle: Instantiate → populate properties →
BeanPostProcessor→ init callbacks → ready → shutdown destroy callbacks. - AOP: JDK dynamic proxies or CGLIB subclass proxies wrap beans — interceptors run before/after/around advised methods (transactions, caching, security).
- ApplicationContext: Superset of BeanFactory — event publishing, internationalization, resource loading, and environment abstraction.
Internal architecture
Spring IoC container and AOP proxy model:
Startup sequence:@Configuration / @ComponentScan│▼BeanDefinitionRegistry (parse @Bean, @Component metadata)│▼BeanFactory / ApplicationContext│├── Instantiate bean (constructor injection)├── Autowire dependencies (resolve by type/name/@Qualifier)├── BeanPostProcessor.beforeInitialization├── @PostConstruct / afterPropertiesSet├── BeanPostProcessor.afterInitialization ← AOP proxy created here└── Bean ready in singleton cacheAOP call path (singleton @Service):Client ──▶ PaymentService$SpringCGLIB$0.debit()│▼TransactionInterceptor (opens TX)│▼Target PaymentService.debit() (your code)Common failure: this.debit() from same class ──▶ NO proxy ──▶ NO @Transactional
Four diagrams — IoC, DI styles, bean lifecycle, and AOP interception:
1. IoC Container
2. Dependency Injection
3. Bean Lifecycle
4. AOP Proxy
Code walkthrough
Spring Core — IoC, constructor DI, lifecycle, and AOP:
- Constructor injection: Final fields, explicit deps, easy unit tests with manual constructor.
- @Transactional on public method: Proxy intercepts external calls — opens/commits JDBC transaction.
- @Aspect + @Around: Cross-cutting audit without modifying PaymentService body.
- @Profile: Environment-specific beans — mock fraud in dev, real HTTP client in prod.
- Self-invocation trap: Calling
this.process()from same class skips proxy — extract to separate bean.
// ── Interfaces — depend on abstractions (DIP) ──public interface LedgerRepository {void save(Payment payment);}public interface FraudChecker {boolean isAllowed(Payment payment);}// ── Implementations — @Component registers as beans ──@Componentpublic class JpaLedgerRepository implements LedgerRepository {@Override public void save(Payment payment) { /* persist */ }}@Componentpublic class HttpFraudChecker implements FraudChecker {@Override public boolean isAllowed(Payment payment) { return true; }}// ── Service — constructor injection (preferred) ──@Servicepublic class PaymentService {private final LedgerRepository ledger;private final FraudChecker fraud;// Single constructor — @Autowired optional since Spring 4.3public PaymentService(LedgerRepository ledger, FraudChecker fraud) {this.ledger = ledger;this.fraud = fraud;}@Transactionalpublic void process(Payment payment) {if (!fraud.isAllowed(payment)) throw new FraudException("blocked");ledger.save(payment);}}// ── Lifecycle hooks ──@Componentpublic class CacheWarmup {@PostConstructpublic void warm() { /* load reference data */ }@PreDestroypublic void shutdown() { /* close connections */ }}// ── AOP — custom audit aspect ──@Aspect@Componentpublic class AuditAspect {@Around("@annotation(Audited)")public Object audit(ProceedingJoinPoint pjp) throws Throwable {long start = System.nanoTime();try {return pjp.proceed();} finally {log.info("Audited {} in {}ms", pjp.getSignature(), (System.nanoTime()-start)/1_000_000);}}}// ── Java configuration (alternative to component scan) ──@Configuration@ComponentScan("com.bank.payments")public class PaymentConfig {@Bean@Profile("!prod")public FraudChecker mockFraud() { return p -> true; }}
Production example
Production payment module — Spring Core patterns at scale:
- READ_COMMITTED + FOR UPDATE: Pessimistic row lock prevents double-debit race in transfer.
- Outbox pattern: Publish event in same TX as ledger write — atomicity without 2PC.
- @Lazy: Injects proxy first — defers real bean creation to break circular graphs.
- @ConditionalOnProperty: Swap fraud vendor without code change — config-driven wiring.
@Service@RequiredArgsConstructor // Lombok generates constructor for final fieldspublic class TransferService {private final AccountRepository accounts;private final OutboxPublisher outbox;private final MeterRegistry metrics;@Transactional(isolation = Isolation.READ_COMMITTED)@Auditedpublic TransferResult transfer(TransferCommand cmd) {Account from = accounts.findByIdForUpdate(cmd.fromId()).orElseThrow(() -> new AccountNotFoundException(cmd.fromId()));from.debit(cmd.amount());accounts.save(from);outbox.publish(new TransferEvent(cmd));metrics.counter("transfers.success").increment();return TransferResult.ok(cmd.id());}}// Circular dependency fix — @Lazy breaks cycle at startup@Servicepublic class NotificationService {public NotificationService(@Lazy EmailService email) { this.email = email; }}// Conditional beans — feature flags@Bean@ConditionalOnProperty(name = "fraud.provider", havingValue = "sift")public FraudChecker siftFraud(SiftClient client) { return new SiftFraudChecker(client); }
Enterprise case study
Global bank — circular dependency production outage: A legacy monolith refactor introduced PaymentService → SettlementService → ReportingService → PaymentService cycle. Spring Boot 3 startup failed with BeanCurrentlyInCreationException. Hotfix at 4 AM: @Lazy on one edge. Proper fix: extract PaymentQueryFacade (read-only) from write path — break cycle by separating CQRS concerns. Secondary lesson: AOP audit aspect wasn't firing on internal package-private methods — CGLIB proxy only advises public methods on proxied beans.
- Symptom: Deploy succeeds locally (different bean order) — fails in prod with circular ref.
- Root cause: Bidirectional service dependencies during "quick refactor."
- Fix: Constructor injection review + arch lint rule forbidding service-to-service cycles.
- AOP gap: Moved audit to interface-based JDK proxy or made methods public on service API.
Performance considerations
Spring Core performance notes:
- Startup time: Large contexts (3000+ beans) slow cold start — use
@Conditional, lazy init sparingly, Spring Boot 3 AOT where applicable. - Singleton default: One instance per container — thread-safe services must not hold mutable request state.
- CGLIB vs JDK proxy: CGLIB subclasses add minor overhead; JDK proxies require interfaces — pick based on design.
- BeanPostProcessor chain: Heavy processors on every bean slow startup — scope processors narrowly.
- Prototype scope: New instance per injection — expensive; use only when stateful per-operation objects required.
Security considerations
Spring Core security implications:
- Field injection: Harder to test, enables null if container misconfigured — prefer constructor for required security beans.
- AOP bypass: Self-invocation skips
@PreAuthorize— security annotations on public API methods only, called externally. - SpEL in @Value: Dynamic expressions can leak env if misused — validate property sources.
- Bean overriding:
spring.main.allow-bean-definition-overriding— accidental override can swap real auth provider with stub.
Scalability considerations
Scaling Spring Core applications:
- Stateless singletons: Scale pods horizontally — no session affinity if services don't hold per-user state.
- Parent/child contexts: Rare in Boot; microservices prefer one context per JVM — avoid shared parent context anti-pattern.
- Event-driven decoupling:
ApplicationEventPublisherbreaks synchronous service cycles — async listeners with@Async. - Custom scopes: Request/session scope doesn't scale across pods without sticky sessions — prefer JWT + stateless.
Production challenges
Real Spring Core production failures:
- Bean not found: Component not scanned — wrong package in
@SpringBootApplicationscan path. - Multiple candidates: Two
FraudCheckerimplementations — fix with@Primaryor@Qualifier. - @Transactional not rolling back: Checked exception swallowed or
rollbackFormissing — TX commits unexpectedly. - Memory leak via @PreDestroy skip: Kill -9 on pod skips graceful shutdown — connection pool not closed.
- Test context cache pollution: Different @MockBean configs — use @DirtiesContext sparingly.
Common mistakes
- Field injection with
@Autowiredon private fields — use constructor injection instead. - Calling advised method via
this— @Transactional/@Cacheable/@Async won't apply. - Using
@Componenton interfaces — won't work; put stereotype on implementation. - Circular dependencies "fixed" with setter injection everywhere — masks design smell.
- @Scope("prototype") injected into singleton — new prototype only at injection time, not per method call.
Debugging guide
Debug Spring Core issues:
- Bean definition report:
--debugorlogging.level.org.springframework=DEBUG— see condition evaluation. - Actuator /beans: List all beans and dependencies in running app.
- Check proxy type:
AopUtils.isAopProxy(bean)— verify AOP applied. - Circular dependency graph: Enable
spring.main.allow-circular-references=falsein dev — fail fast. - @Transactional debug:
logging.level.org.springframework.transaction=TRACE— see begin/commit/rollback.
# application-dev.ymllogging:level:org.springframework.beans.factory: DEBUGorg.springframework.transaction: TRACE# Inspect beancurl localhost:8080/actuator/beans | jq '.contexts.application.beans.paymentService'# Verify AOP in testassertTrue(AopUtils.isAopProxy(transferService));
Best practices
- Constructor injection for required dependencies — final fields, immutable services.
- Program to interfaces —
LedgerRepositorynotJpaLedgerRepositoryin service signature. - Keep @Configuration classes focused — one module per config class.
- Use @Qualifier or custom meta-annotations when multiple implementations exist.
- Externalize cross-cutting concerns to AOP/aspects — not copy-paste in every service method.
- Fail startup on missing beans in prod — avoid @Autowired(required=false) on critical deps.
- Document bean scopes explicitly when not singleton.
Anti-patterns
- Service locator pattern —
context.getBean()hides dependencies. - Static ApplicationContext holder — untestable global state.
- God @Configuration with 50 @Bean methods — split by domain module.
- @Transactional on private methods — never advised, silent no-op.
- Using @ComponentScan on every test — prefer @WebMvcTest slice tests.
Staff engineer notes
- Spring Core is not magic — it is a dependency graph builder plus proxy factory. Read the startup condition report once; it saves hours.
- Circular dependencies signal missing boundaries — extract read models or events instead of @Lazy forever.
- AOP proxy behavior explains 80% of "annotation doesn't work" tickets — always ask "who calls this method?"
- Constructor injection makes immutability and null-safety the default — align with records and sealed types in Java 21.
- In code review: any @Autowired field on @Service is a comment requesting constructor refactor.
Interview questions
Interview preparation
15 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What is Inversion of Control (IoC)?
BeginnerModel answer
Design principle where object creation and dependency management move from application code to a framework container.
Instead of classes calling new on collaborators, the IoC container (Spring ApplicationContext) instantiates beans, resolves dependencies, and manages lifecycle.
Promotes loose coupling and testability.
Follow-up probe
IoC vs DI?
2What is Dependency Injection and why prefer constructor injection?
BeginnerModel answer
DI supplies dependencies from outside rather than class creating them.
Constructor injection: dependencies required at creation, fields can be final/immutable, easy to unit test without Spring, null-safe.
Field injection hides dependencies and complicates testing.
Setter injection for optional deps only.
Follow-up probe
@Autowired on constructor?
3Explain the Spring bean lifecycle.
BeginnerModel answer
1) Instantiate via constructor 2) Populate properties/@Autowired 3) BeanNameAware/BeanFactoryAware callbacks 4) BeanPostProcessor.postProcessBeforeInitialization 5) @PostConstruct or InitializingBean.afterPropertiesSet 6) BeanPostProcessor.postProcessAfterInitialization (AOP proxy often here) 7) Bean in use 8) @PreDestroy or DisposableBean.destroy on shutdown.
Follow-up probe
BeanPostProcessor purpose?
4What is AOP in Spring?
BeginnerModel answer
Aspect-Oriented Programming separates cross-cutting concerns (logging, transactions, security) from business logic.
Spring AOP uses proxies (JDK dynamic proxy for interfaces, CGLIB for classes) to wrap beans.
Advice (@Before, @After, @Around) runs at join points matched by pointcuts.
@Transactional is declarative TX advice.
Follow-up probe
Join point vs pointcut?
5Difference between BeanFactory and ApplicationContext?
BeginnerModel answer
- BeanFactory is core IoC container
- lazy bean creation by default. ApplicationContext extends BeanFactory adding event propagation, internationalization, resource loading, automatic BeanPostProcessor registration, and eager singleton init. Spring Boot uses ApplicationContext (AnnotationConfigApplicationContext).
Follow-up probe
When lazy init?
Intermediate
6Why doesn't @Transactional work on self-invocation?
IntermediateModel answer
- Spring applies @Transactional via proxy wrapping the bean. External caller invokes proxy.debit() which starts TX then calls target. Internal this.debit() calls target directly
- bypasses proxy, no TX interceptor. Fix: inject self interface, extract to separate bean, or use AspectJ compile-time weaving (rare).
Follow-up probe
CGLIB vs JDK proxy?
7How resolve multiple beans of same type?
IntermediateModel answer
@Primary marks default candidate. @Qualifier('beanName') selects by name. @Resource(name) JSR-250 by name. Custom qualifier annotations (@SiftFraud) meta-annotate @Qualifier. Best: single interface per impl or use Mapinjection for strategy pattern. Follow-up probe
Optional dependency?
8What causes circular dependency and how fix?
IntermediateModel answer
- Bean A constructor needs B, B constructor needs A
- Spring cannot determine creation order. Fixes: redesign (extract facade, events), @Lazy on one dependency (proxy breaks cycle), setter injection (not recommended long-term). Spring Boot 2.6+ disables circular refs by default.
Follow-up probe
Is @Lazy a crutch?
9Explain @Configuration vs @Component.
IntermediateModel answer
- @Configuration is specialized @Component
- class defines @Bean methods. Full @Configuration uses CGLIB to intercept @Bean methods so repeated calls return same singleton. @Component marks stereotype for auto-detection. @Bean on @Configuration method registers factory-produced bean.
Follow-up probe
proxyBeanMethods=false?
10What are bean scopes?
IntermediateModel answer
singleton (default, one per container), prototype (new each getBean/injection point), request/session/application (web). Singleton services must be thread-safe. Prototype in singleton gets one instance at injection — use ObjectProvideror @Lookup for per-call instances. Follow-up probe
ThreadLocal in singleton?
Advanced
11Compare JDK dynamic proxy and CGLIB.
AdvancedModel answer
- JDK proxy: requires interface, implements all interface methods, InvocationHandler dispatches. CGLIB: subclasses concrete class, final methods cannot be advised. Spring Boot defaults CGLIB for class-based beans. Performance similar on modern JVM
- design choice is interface-first vs concrete class.
Follow-up probe
final class @Transactional?
12Design testable PaymentService without Spring in unit test.
AdvancedModel answer
Constructor inject LedgerRepository and FraudChecker interfaces.
Unit test: new PaymentService(mockLedger, mockFraud).
Integration test: @SpringBootTest with @MockBean for external HTTP.
Avoid field injection and static context.
Use Testcontainers for DB integration.
Follow-up probe
@MockBean vs @Mock?
13How does @Around advice differ from @Before/@After?
AdvancedModel answer
- @Around wraps join point
- controls whether proceed() runs, can modify return/exception, measure timing, implement retry. @Before runs before method, cannot prevent execution easily. @AfterReturning/@AfterThrowing react to outcome. @Around most powerful
- used for transactions, caching, circuit breakers.
Follow-up probe
ProceedingJoinPoint?
14Architect module boundaries to avoid circular deps.
AdvancedModel answer
Layer: controller → application service → domain → infrastructure.
Domain never depends on Spring.
Use events (TransferCompleted) for downstream reactions instead of ReportingService calling PaymentService.
CQRS split: PaymentCommandService vs PaymentQueryService.
Package-private domain, public API interfaces only.
Follow-up probe
Hexagonal architecture?
15Production incident: bean startup 90s — how investigate?
AdvancedModel answer
Enable startup Actuator endpoint or Spring Boot 3 startup steps.
Check @PostConstruct doing network I/O.
Review @ComponentScan importing unused modules.
Conditional beans evaluating slow Environment checks.
Consider lazy-init for non-critical beans.
AOT/native-image for serverless cold start.
Follow-up probe
spring-context-indexer?
Hands-on exercise
Lab: Spring Core concepts
- Define PaymentService with constructor-injected mock repositories — unit test without Spring.
- Add @Transactional to process() — verify rollback on RuntimeException with @DataJpaTest or integration test.
- Create @Aspect audit logging on @Audited methods — confirm proxy via AopUtils.
- Simulate circular dependency — fix with @Lazy then refactor to event publisher.
- Add @PostConstruct warmup and @PreDestroy cleanup — observe logs on context start/stop.
JavaSpring Core: IoC, DI, Bean Lifecycle, AOP
Press Run to execute. Real javac + JVM via remote API. Falls back to simulator on network failure.Architecture trade-offs
- Constructor vs field injection: Constructor wins for required deps; field only for legacy/framework constraints.
- Interface vs class proxies: Interfaces enable JDK proxies and cleaner testing; concrete classes need CGLIB.
- XML vs Java config: Java/@Configuration standard in 2026; XML for legacy only.
- Full context vs slice tests: @WebMvcTest faster but less coverage — balance CI speed vs confidence.
Summary
Spring Core is the dependency injection and proxy foundation beneath every Spring Boot service. Master IoC, constructor DI, bean lifecycle, and AOP interception — and you can debug startup failures, transaction bugs, and cross-cutting concerns with confidence. Next: Spring Boot auto-configuration and production operability.
Key takeaways
- IoC container builds dependency graph — prefer constructor injection and program to interfaces.
- Bean lifecycle: instantiate → autowire → init → AOP proxy → ready → destroy.
- AOP proxies intercept external calls — self-invocation bypasses @Transactional and @Cacheable.
- Circular dependencies are design smells — refactor before @Lazy becomes permanent.
- Spring Core fundamentals explain Spring Boot auto-config and most production wiring bugs.