Java Fundamentals Tutorial 0/33 lessons ~6 min read Lesson 20

    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…

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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 new wiring 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: @Transactional or @Cacheable "doesn't work" because method called via this — 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 @MockBean would 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:

    text
    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 cache
    AOP 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

    ApplicationContext wiring
    @Configuration
    Java config
    Bean definitions
    Metadata
    Container
    Creates & caches
    @Autowired beans
    Injected graph
    You declare components; Spring builds and holds the object graph.

    2. Dependency Injection

    Constructor injection (preferred)
    PaymentService
    Depends on
    LedgerRepo
    Interface
    FraudClient
    Interface
    Container
    Wires at startup
    Constructor injection — immutable, testable, explicit required deps.

    3. Bean Lifecycle

    Singleton bean phases
    Instantiate
    Constructor
    Populate
    @Autowired
    Init
    @PostConstruct
    Proxy
    AOP wrap
    BeanPostProcessors can wrap bean before it enters singleton cache.

    4. AOP Proxy

    @Transactional interception
    Caller
    External
    Proxy
    CGLIB/JDK
    Interceptor
    TX advice
    Target method
    Business logic
    External calls hit proxy; self-invocation bypasses advice.

    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.
    java
    // ── Interfaces — depend on abstractions (DIP) ──
    public interface LedgerRepository {
    void save(Payment payment);
    }
    public interface FraudChecker {
    boolean isAllowed(Payment payment);
    }
    // ── Implementations — @Component registers as beans ──
    @Component
    public class JpaLedgerRepository implements LedgerRepository {
    @Override public void save(Payment payment) { /* persist */ }
    }
    @Component
    public class HttpFraudChecker implements FraudChecker {
    @Override public boolean isAllowed(Payment payment) { return true; }
    }
    // ── Service — constructor injection (preferred) ──
    @Service
    public class PaymentService {
    private final LedgerRepository ledger;
    private final FraudChecker fraud;
    // Single constructor — @Autowired optional since Spring 4.3
    public PaymentService(LedgerRepository ledger, FraudChecker fraud) {
    this.ledger = ledger;
    this.fraud = fraud;
    }
    @Transactional
    public void process(Payment payment) {
    if (!fraud.isAllowed(payment)) throw new FraudException("blocked");
    ledger.save(payment);
    }
    }
    // ── Lifecycle hooks ──
    @Component
    public class CacheWarmup {
    @PostConstruct
    public void warm() { /* load reference data */ }
    @PreDestroy
    public void shutdown() { /* close connections */ }
    }
    // ── AOP — custom audit aspect ──
    @Aspect
    @Component
    public 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.
    java
    @Service
    @RequiredArgsConstructor // Lombok generates constructor for final fields
    public class TransferService {
    private final AccountRepository accounts;
    private final OutboxPublisher outbox;
    private final MeterRegistry metrics;
    @Transactional(isolation = Isolation.READ_COMMITTED)
    @Audited
    public 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
    @Service
    public 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: ApplicationEventPublisher breaks 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 @SpringBootApplication scan path.
    • Multiple candidates: Two FraudChecker implementations — fix with @Primary or @Qualifier.
    • @Transactional not rolling back: Checked exception swallowed or rollbackFor missing — 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 @Autowired on private fields — use constructor injection instead.
    • Calling advised method via this — @Transactional/@Cacheable/@Async won't apply.
    • Using @Component on 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: --debug or logging.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=false in dev — fail fast.
    • @Transactional debug: logging.level.org.springframework.transaction=TRACE — see begin/commit/rollback.
    bash
    # application-dev.yml
    logging:
    level:
    org.springframework.beans.factory: DEBUG
    org.springframework.transaction: TRACE
    # Inspect bean
    curl localhost:8080/actuator/beans | jq '.contexts.application.beans.paymentService'
    # Verify AOP in test
    assertTrue(AopUtils.isAopProxy(transferService));

    Best practices

    • Constructor injection for required dependencies — final fields, immutable services.
    • Program to interfaces — LedgerRepository not JpaLedgerRepository in 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

    5
    1. 1What is Inversion of Control (IoC)?
      Beginner

      Model 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?

    2. 2What is Dependency Injection and why prefer constructor injection?
      Beginner

      Model 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?

    3. 3Explain the Spring bean lifecycle.
      Beginner

      Model 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?

    4. 4What is AOP in Spring?
      Beginner

      Model 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?

    5. 5Difference between BeanFactory and ApplicationContext?
      Beginner

      Model 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

    5
    1. 6Why doesn't @Transactional work on self-invocation?
      Intermediate

      Model 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?

    2. 7How resolve multiple beans of same type?
      Intermediate

      Model 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 Map injection for strategy pattern.

      Follow-up probe

      Optional dependency?

    3. 8What causes circular dependency and how fix?
      Intermediate

      Model 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?

    4. 9Explain @Configuration vs @Component.
      Intermediate

      Model 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?

    5. 10What are bean scopes?
      Intermediate

      Model 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 ObjectProvider or @Lookup for per-call instances.

      Follow-up probe

      ThreadLocal in singleton?

    Advanced

    5
    1. 11Compare JDK dynamic proxy and CGLIB.
      Advanced

      Model 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?

    2. 12Design testable PaymentService without Spring in unit test.
      Advanced

      Model 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?

    3. 13How does @Around advice differ from @Before/@After?
      Advanced

      Model 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?

    4. 14Architect module boundaries to avoid circular deps.
      Advanced

      Model 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?

    5. 15Production incident: bean startup 90s — how investigate?
      Advanced

      Model 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

    Starter Templates
    OutputRemote JVM (Piston · Java 15)
    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.
    Ready to mark this lesson complete?Track your journey across the entire course.