Spring Internals and Interview Preparation - Complete Notes

A practical guide to how Spring creates beans, resolves dependencies, applies post-processors, builds proxies, selects auto-configuration, publishes startup events, diagnoses failures, and turns framework knowledge into strong interview and system-design reasoning.

00. The internal mental model

Spring is best understood as a metadata-driven object factory plus a set of extension points that can change metadata, create objects, inject dependencies, run lifecycle callbacks, and wrap objects with behavior.

Application code usually sees ordinary Java objects. The important difference is that the container controls how managed objects come into existence. Configuration declares what should exist. The bean factory stores that declaration as metadata. During creation, Spring resolves a dependency graph, constructs each object, populates it, invokes lifecycle callbacks, and lets infrastructure inspect or replace the object. The reference finally stored as a singleton may be the original object or a proxy around it.

Think of a restaurant kitchen. A bean definition is the recipe, the bean factory is the kitchen, dependency resolution gathers ingredients, lifecycle callbacks perform preparation, and post-processors are quality stations that may label, decorate, or package the result. A proxy is the service counter through which a request can be checked, timed, authorized, or placed in a transaction before it reaches the prepared object.

Layer Main responsibility Typical types
Definition metadata Describe class, factory, scope, dependencies, qualifiers, and lifecycle methods. BeanDefinition, BeanDefinitionRegistry
Object factory Resolve, create, cache, and return managed objects. BeanFactory, DefaultListableBeanFactory
Application container Add events, resources, messages, environment, and automatic extension discovery. ApplicationContext
Metadata extension Change definitions before ordinary beans are created. BeanFactoryPostProcessor
Instance extension Inspect, initialize, or replace individual bean instances. BeanPostProcessor
Invocation extension Run cross-cutting behavior around method calls. Spring AOP proxy, interceptor, advice
Boot assembly Select sensible infrastructure from classpath, beans, properties, and environment. Auto-configuration and conditional annotations
Three questions solve many Spring mysteries
  1. Is this object actually managed by the same application context?
  2. Is the caller invoking the object through the Spring-provided reference or directly?
  3. At which lifecycle phase is this code running?

01. ApplicationContext refresh pipeline

Refresh is the coordinated process that turns configuration metadata into a usable application context.

Exact implementation details vary between context types and framework versions, but the durable sequence is stable. The context prepares internal state and the environment, obtains or refreshes its bean factory, loads definitions, invokes factory-level extension points, registers instance-level post-processors, initializes infrastructure such as messages and events, pre-instantiates eligible singletons, and finally publishes completion events.

Phase What happens Why ordering matters
Prepare context Validate properties, initialize environment, and prepare early event handling. Later processors need stable environment and context state.
Load definitions Register configuration classes, scanned components, imports, and explicit beans. Spring must know the candidate graph before creating ordinary singletons.
Process definitions Invoke registry and bean-factory post-processors. They may register or change definitions before instances exist.
Register bean post-processors Sort and install processors that will participate in instance creation. A bean created too early may miss annotation handling or proxying.
Initialize context infrastructure Prepare message source, event multicaster, listeners, and context-specific services. Lifecycle and application code may depend on these facilities.
Create non-lazy singletons Resolve dependency graphs and complete bean lifecycles. Eager creation finds configuration failures before traffic arrives.
Finish refresh Start lifecycle components and publish refresh or Boot readiness events. Consumers should not treat the application as ready too early.

Refresh is not merely a loop over classes. Configuration parsing can discover new configuration, imports can add more definitions, post-processors can register infrastructure, and creating one bean can recursively create its dependencies. This is why startup traces often look like a graph rather than a flat list.

BeanFactory versus ApplicationContext

BeanFactory is the core container contract. ApplicationContext builds on it with automatic processor discovery, events, resource loading, internationalization, and application integration. Most applications should use an ApplicationContext.

02. From configuration to BeanDefinition

Spring normally discovers metadata first and creates application objects later.

A BeanDefinition records how a bean should be created and managed. It can contain an implementation class or factory method, constructor arguments, property values, scope, lazy flag, qualifiers, primary status, initialization and destruction methods, dependencies, and other attributes. It is a recipe, not the bean instance.

Definitions can come from component scanning, @Bean methods, imported configuration, XML, programmatic registrars, or Boot auto-configuration. ConfigurationClassPostProcessor is a factory-level processor that parses configuration classes, component scans, @Import, and @Bean methods and registers the resulting definitions.

Java configuration becomes two managed definitions
@Configuration(proxyBeanMethods = false)
class BillingConfiguration {

    @Bean
    ExchangeRateClient exchangeRateClient(HttpClient httpClient) {
        return new ExchangeRateClient(httpClient);
    }

    @Bean
    InvoiceService invoiceService(ExchangeRateClient exchangeRateClient) {
        return new InvoiceService(exchangeRateClient);
    }
}

Method parameters express dependencies without calling another @Bean method. This works with both full and lite configuration modes. In full mode, direct calls between @Bean methods can be intercepted through a generated CGLIB subclass so calls respect container scope. With proxyBeanMethods = false, direct calls are normal Java calls and create normal objects. Method-parameter injection avoids that surprise and reduces configuration proxy overhead.

FactoryBean is not the same as a factory method

A FactoryBean<T> is a special managed bean whose getObject() result is exposed under its bean name. Calling getBean("reportClient") returns the product, while getBean("&reportClient") returns the factory itself. Framework integrations use this pattern when object creation is complex. Do not confuse it with an ordinary @Bean factory method.

03. Dependency resolution internals

Autowiring is a candidate-selection algorithm, not a magical field assignment.

For a constructor or factory-method parameter, Spring creates a dependency descriptor containing the required type, generic information, annotations, declaring element, and whether the dependency is required. The bean factory finds type-compatible candidates, removes candidates that are not autowire candidates, applies qualifiers, considers @Primary and priority, uses name matching when appropriate, and either selects one result or reports ambiguity.

Injection shape Resolution behavior Useful purpose
PaymentGateway Requires one matching candidate after qualifiers and priority rules. A mandatory collaboration.
Optional<PaymentGateway> Supplies a candidate when present without requiring one. Optional application capability.
ObjectProvider<PaymentGateway> Defers lookup and supports optional, ordered, or stream access. Lazy or programmatic selection without storing the context.
List<PaymentGateway> Injects all compatible candidates, ordered by Spring ordering rules. Strategy chain or plugin collection.
Map<String, PaymentGateway> Injects candidates keyed by bean name. Named strategy registry.
@Qualifier("card") Narrows candidates using qualifier metadata. Stable semantic selection.

Generic types are part of modern resolution. For example, Handler<Invoice> and Handler<Refund> can be distinct candidates when their generic metadata is available. Type erasure, raw types, broad factory return types, or proxies can make runtime type prediction harder. Declare the most specific practical return type on @Bean methods, especially when conditions or autowiring must inspect it before instantiation.

Do not solve every ambiguity with @Primary

@Primary expresses a default, not business routing. If the choice depends on tenant, transaction, request, feature, or data, inject a strategy registry and make the decision explicitly. Hidden selection rules make systems hard to test and operate.

04. Bean creation lifecycle in detail

A singleton becomes visible as a fully initialized managed reference only after a multi-stage creation pipeline succeeds.

  1. Resolve merged definition: combine parent metadata, scope, factory information, and definition attributes.
  2. Choose construction path: select a constructor, factory method, supplier, or factory bean.
  3. Instantiate: allocate the raw object. Advanced post-processors can participate before or during constructor selection.
  4. Populate dependencies: resolve constructor arguments during construction and inject annotated fields, methods, or configured properties at the appropriate processor phase.
  5. Invoke aware callbacks: supply container infrastructure such as bean name, factory, class loader, environment, or application context when explicitly requested.
  6. Run before-initialization processors: processors may invoke lifecycle annotations or adapt state before init methods.
  7. Run initialization callbacks: normally @PostConstruct, then InitializingBean.afterPropertiesSet(), then a custom init method when these are different methods.
  8. Run after-initialization processors: processors may return the object or replace it with a proxy.
  9. Publish managed reference: cache the completed singleton and return the final reference to callers.
  10. Destroy on context close: normally @PreDestroy, then DisposableBean.destroy(), then a custom destroy method when applicable.
A lifecycle bean with framework-independent callbacks
@Component
final class RulesIndex {
    private final RulesRepository repository;
    private Map<String, Rule> rules = Map.of();

    RulesIndex(RulesRepository repository) {
        this.repository = repository;
    }

    @PostConstruct
    void load() {
        this.rules = Map.copyOf(repository.loadEnabled());
    }

    @PreDestroy
    void clear() {
        this.rules = Map.of();
    }
}

Constructor code runs before field and setter injection, so injected members are not ready there. @PostConstruct runs after dependency population but before the final post-processing result is necessarily returned to other beans. Avoid slow remote calls, unbounded work, and calls that depend on this bean's own proxy behavior. Startup should be deterministic and bounded.

After all regular singletons

SmartInitializingSingleton.afterSingletonsInstantiated() runs after regular singleton pre-instantiation finishes. It is useful when infrastructure must inspect a complete singleton set without forcing accidental early initialization. It still belongs to startup, so failures can fail refresh and long work delays readiness.

Prototype destruction is the caller's responsibility

Spring creates and configures prototype beans but does not track their full lifetime after returning them. Destruction callbacks are therefore not automatically invoked for prototypes. The consumer that owns the prototype resource must close or dispose it. Use singleton-managed resource pools rather than prototypes for most expensive resources.

05. Bean factory and bean post-processors

Post-processors are the mechanism behind much of Spring's apparent annotation magic.

Extension Operates on Runs Typical use
BeanDefinitionRegistryPostProcessor Definition registry Before other factory post-processors Register additional definitions.
BeanFactoryPostProcessor Definition metadata and factory Before ordinary bean creation Resolve placeholders or alter scopes and properties.
BeanPostProcessor Bean instances During every eligible bean lifecycle Annotation callbacks, validation, adaptation, proxy creation.
InstantiationAwareBeanPostProcessor Construction and properties Before and after instantiation, before population Annotated injection and custom property handling.
SmartInstantiationAwareBeanPostProcessor Candidate constructors and early references Advanced creation phases Constructor prediction and consistent early proxy references.

AutowiredAnnotationBeanPostProcessor processes @Autowired and @Value. A common-annotation processor handles callbacks such as @PostConstruct and @PreDestroy. Auto-proxy creators are also post-processors. An annotation has behavior only because some registered infrastructure reads it.

A small diagnostic BeanPostProcessor
@Component
final class CriticalClientVerifier implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String name) {
        Class<?> targetType = AopUtils.getTargetClass(bean);
        if (targetType.isAnnotationPresent(CriticalClient.class)) {
            verifyConfiguration(name, targetType);
        }
        return bean;
    }
}
Post-processors are infrastructure, not ordinary services

They are created early. Dependencies directly pulled into a post-processor may also be created early and become ineligible for the complete processor chain, including auto-proxying. Keep processors small, avoid broad autowiring, declare factory-level processors through static @Bean methods when appropriate, and never perform network work in them.

06. Circular references and early exposure

A dependency cycle is usually a design signal, and constructor cycles are not resolvable because neither object can be constructed first.

For some singleton property-injection cycles, the core container can expose an early reference after instantiation but before full initialization. Bean A can receive an early reference to B while B is being completed, or the reverse. Advanced auto-proxy infrastructure must ensure that this early reference is compatible with the final proxied reference. The mechanism is complex and means a collaborator can observe an object before its lifecycle finishes.

Spring Boot applications should not depend on automatic circular-reference recovery. Modern Boot behavior rejects circular references by default, and enabling them can still fail for constructor cycles or proxy combinations. Fix the graph instead of treating the setting as an architecture option.

Cause Better redesign
Two services coordinate each other. Extract an orchestration service above both.
Domain logic calls infrastructure which calls domain logic. Use ports, events, or a result returned to the outer layer.
Shared mutable state needs callbacks. Give one component ownership and expose narrow operations.
Initialization order is being modeled as dependency. Use explicit startup orchestration or @DependsOn only for a real order.
Rare callback needs the other service. Consider an event or narrowly scoped ObjectProvider, then document it.

07. The Spring proxy mental model

A proxy works only when a call crosses the proxy boundary.

A Spring AOP proxy holds or delegates to a target and runs an interceptor chain around eligible method calls. Transaction management, method security, caching, retry integrations, and async execution commonly use this design. The container injects the proxy as the managed reference. Calling a method through that reference allows advice to run.

External call is advised, self-invocation is not
@Service
class InvoiceService {

    @Transactional
    public void issue(InvoiceCommand command) {
        persist(command);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void persist(InvoiceCommand command) {
        // This call is direct when invoked through this.persist(...).
    }
}

A client calling invoiceService.issue(...) crosses the proxy, so transaction advice can wrap issue. The internal call from issue to persist uses this, which is the target object. It does not return to the proxy, so the REQUIRES_NEW declaration is not applied.

Prefer refactoring over proxy self-access

Move the separately advised operation to another bean and inject it. This makes the boundary visible and testable. Self-injection or AopContext.currentProxy() couples business code to proxy mechanics and is rarely a good production design.

08. JDK proxies versus CGLIB proxies

Property JDK dynamic proxy CGLIB class proxy
Mechanism Generated object implements one or more interfaces. Generated subclass extends the target class.
Advisable API Interface methods visible through the proxy. Overridable methods on the class.
Type seen by callers Usually inject by interface. Assignable to the concrete target class.
Important limits Concrete-only methods are not part of the proxy contract. Final class cannot be subclassed; final or private methods cannot be overridden.
Module concerns Uses standard Java proxy facilities. Subclass generation may face module-access restrictions.
Self-invocation Bypasses advice. Also bypasses advice for calls made on this.

The most important distinction is the type surface, not a simplistic performance claim. Program to meaningful interfaces when they represent an architectural boundary. Do not create interfaces only to obtain a proxy. Class proxies are appropriate for concrete services, but proxyable methods must not be final. Always test the behavior through the Spring-managed bean, not by directly constructing the target.

Proxy identity and runtime inspection

bean.getClass() may return a generated proxy class, so exact class comparisons and annotation lookups on the runtime class can fail. Use Spring utilities such as AopUtils.getTargetClass(bean) when infrastructure needs the underlying type. Domain equality should not depend on proxy class identity. JPA entities have additional proxy and equality concerns and should use a carefully designed identity strategy.

09. AOP terminology and interceptor chains

Spring AOP applies cross-cutting behavior to method-execution join points on Spring-managed beans.

Term Meaning Example
Aspect A module containing cross-cutting policy. Audit policy or latency measurement.
Join point A point where behavior could apply. In Spring AOP, method execution is central. Calling checkout() through a proxy.
Pointcut A rule selecting join points. Public methods annotated with @Audited.
Advice Code run at selected join points. Before, after returning, after throwing, after finally, or around.
Advisor A pointcut plus advice. Transaction attribute source plus transaction interceptor.
Target The object containing business behavior. CheckoutService.
Proxy The object exposed to clients that runs the interceptor chain. Generated JDK or CGLIB object.
Weaving Linking aspects to code. Spring AOP does it through runtime proxies. Different from AspectJ bytecode weaving.
An around advice that preserves exceptions and return values
@Aspect
@Component
final class TimingAspect {
    private final MeterRegistry registry;

    TimingAspect(MeterRegistry registry) {
        this.registry = registry;
    }

    @Around("@annotation(MeasuredOperation)")
    Object time(ProceedingJoinPoint call) throws Throwable {
        Timer.Sample sample = Timer.start(registry);
        try {
            return call.proceed();
        } finally {
            sample.stop(registry.timer(
                    "application.operation",
                    "method", call.getSignature().getName()));
        }
    }
}

Around advice is powerful because it controls whether and when the target runs. It can also break semantics by swallowing exceptions, calling proceed() twice, changing arguments, or returning an incompatible value. Use the narrowest advice type that solves the need.

Advice ordering

Multiple advisors can wrap one method. Ordering decides which is outermost on entry and therefore last on exit. For example, retry outside transaction can create a fresh transaction per attempt, while transaction outside retry can repeat attempts inside one transaction that may already be rollback-only. Security, tracing, caching, retry, and transactions need an intentional order and an integration test that proves the required semantics.

10. How annotations become behavior

Java annotations are metadata. Scanners, registrars, post-processors, advisors, or compilers must interpret them.

Annotation Primary interpreter Result
@Component stereotypes Classpath scanner and configuration parser Bean definition registration.
@Configuration, @Bean, @Import ConfigurationClassPostProcessor Configuration model and bean definitions.
@Autowired, @Value AutowiredAnnotationBeanPostProcessor Dependency and value injection.
@PostConstruct, @PreDestroy Lifecycle annotation post-processor Initialization and destruction callbacks.
@Transactional Transaction attribute source, advisor, and interceptor Transactional proxy behavior.
@Async Async annotation post-processor and interceptor Executor submission through a proxy.
Boot @Conditional... Configuration condition evaluation Include or skip configuration metadata.

Meta-annotations allow a custom annotation to combine semantics. Spring searches merged annotation metadata, including composed annotations and aliases. Design composed annotations sparingly. Hiding ten unrelated policies behind one annotation makes behavior difficult to understand and upgrade.

Reflection, generated metadata, and AOT

Traditional Spring applications can inspect classes and annotations at runtime. Build-time processors and ahead-of-time processing can precompute hints and generated code to reduce runtime discovery and support native images. Dynamic class loading, reflection, resource lookup, and proxy generation may need explicit runtime hints in native deployments. A feature that works on the JVM is not automatically native-compatible.

11. Spring Boot auto-configuration internals

Auto-configuration is conditional configuration chosen from known candidates, not runtime guessing.

@SpringBootApplication combines Boot configuration, component scanning, and auto-configuration enablement. Boot imports candidate auto-configuration classes listed by libraries in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. Each candidate is ordinary configuration guarded by conditions. Candidates that do not match are skipped; matching candidates contribute bean definitions.

A small library auto-configuration
@AutoConfiguration
@ConditionalOnClass(NotificationClient.class)
@ConditionalOnProperty(
        prefix = "notification",
        name = "enabled",
        havingValue = "true",
        matchIfMissing = true)
public class NotificationAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    NotificationClient notificationClient(NotificationProperties properties) {
        return new HttpNotificationClient(properties.endpoint());
    }
}

The corresponding imports file contains the fully qualified auto-configuration class name. A starter usually supplies dependencies and points users toward auto-configuration, while the auto-configuration module contains the conditional configuration itself.

Back-off is a contract

@ConditionalOnMissingBean lets user configuration replace a default. This is why declaring a custom DataSource, security chain, executor, or mapper can cause Boot to back off. It is intentional when the condition report confirms it.

Ordering does not create dependencies

@AutoConfigureBefore and @AutoConfigureAfter influence definition processing order between auto-configurations. They do not guarantee bean initialization order. Express real bean dependencies through constructor or method parameters. Use @DependsOn only for an initialization dependency that cannot be represented as an injected collaboration.

12. Conditional annotations and their traps

Condition Question Common trap
@ConditionalOnClass Is a class available? Method signatures may load an absent type too early. Isolate configuration.
@ConditionalOnMissingBean Has the user already supplied a compatible bean? Evaluation sees definitions processed so far, so use it mainly in auto-configuration.
@ConditionalOnBean Does required infrastructure exist? A bean may exist in a parent or have a less specific declared return type.
@ConditionalOnProperty Does configuration request this feature? Missing, blank, and false values can differ. State defaults explicitly.
@ConditionalOnResource Is a resource present? Classpath and filesystem locations behave differently after packaging.
@ConditionalOnWebApplication Is this a servlet or reactive web application? Tests may create a different context kind than production.
@ConditionalOnExpression Does a SpEL expression evaluate to true? Referencing a bean can initialize it too early and skip full post-processing.

Test auto-configuration as a matrix of classpath, property, and user-bean conditions. ApplicationContextRunner creates small contexts and supports filtered class loaders, property values, user configuration, and condition report logging. A single happy-path @SpringBootTest does not prove back-off behavior.

13. Spring Boot startup events

Startup events describe milestones, and a listener must be registered before the milestone it wants to observe.

Event Meaning Appropriate use
ApplicationStartingEvent Run has begun before most processing. Very early infrastructure registered outside the context.
ApplicationEnvironmentPreparedEvent Environment is ready before the context exists. Early environment inspection or customization.
ApplicationContextInitializedEvent Context initializers have run, definitions are not yet fully loaded. Context-level bootstrapping.
ApplicationPreparedEvent Definitions are loaded and refresh has not completed. Advanced startup infrastructure.
ApplicationStartedEvent Context refreshed, before runners complete. Measure post-refresh startup work.
ApplicationReadyEvent Runners completed and the application is ready to serve. Small final readiness actions and startup measurement.
ApplicationFailedEvent Startup failed. Last-resort failure reporting with minimal dependencies.

Very early events occur before listener beans can exist, so register listeners on SpringApplication, through a builder, or with supported library metadata. Do not start important business workflows merely because a refresh event arrived. Multiple application contexts can publish similar framework events, and a listener should check its context and idempotency.

Startup profiling

Enable --debug or the condition-report logger to understand configuration decisions. Use BufferingApplicationStartup with the Actuator startup endpoint for structured startup steps, or FlightRecorderApplicationStartup to correlate Spring startup with class loading, allocation, garbage collection, and JVM activity. Compare repeated measurements with the same classpath and machine conditions.

14. @Transactional proxy limitations

Transaction annotations describe policy, while an interceptor and transaction manager implement that policy around an intercepted invocation.

In default proxy mode, the call must enter through the proxy. Self-invocation does not apply a new declaration. A directly constructed object is not transactional. A method invoked during @PostConstruct should not depend on the bean's transaction proxy being complete. Method visibility and proxy type also matter. Interface-based proxies expose public interface methods. Class-based proxies can support additional non-private methods, but final and private methods cannot be overridden.

Symptom Likely cause Focused fix
No transaction on an annotated method. Object created with new, self-call, wrong context, or method not proxyable. Call a Spring-managed bean across an explicit service boundary.
REQUIRES_NEW behaves like the outer transaction. The inner method was called on this. Move the inner boundary to another bean.
Transaction disappears in a new thread. Imperative transactions are normally thread-bound. Start a new explicit transaction in the worker or redesign the workflow.
Unexpected rollback at commit. Inner work marked the shared transaction rollback-only and the exception was caught. Choose propagation and exception policy deliberately.
Annotation on interface is inconsistent across modes. Annotation discovery and proxy or weaving strategy differ. Prefer annotations on concrete implementation methods or classes.

AspectJ mode can weave transactional behavior into bytecode and therefore cover self-invocation, but it adds build or load-time complexity. It should be an explicit architecture choice, not a quick repair for unclear service boundaries.

15. @Async proxy limitations

@Async normally means an interceptor submits the invocation to a TaskExecutor; it does not make arbitrary code automatically asynchronous.

  • The call must cross the proxy, so self-invocation runs synchronously.
  • @Async cannot be relied on in the same bean's lifecycle callback.
  • Private methods cannot be intercepted by the normal proxy mechanism.
  • void methods cannot deliver exceptions to the caller. Configure an AsyncUncaughtExceptionHandler or return CompletableFuture.
  • Thread-local state such as imperative transactions, security context, MDC, locale, and request attributes does not automatically cross every executor boundary.
  • An unbounded executor queue can accept work faster than the application can finish it, producing memory growth and extreme latency instead of backpressure.
Explicit async boundary with a named executor
@Service
class ReceiptDispatcher {

    @Async("receiptExecutor")
    CompletableFuture<DispatchResult> dispatch(Receipt receipt) {
        return CompletableFuture.completedFuture(send(receipt));
    }
}

@Bean
ThreadPoolTaskExecutor receiptExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(4);
    executor.setMaxPoolSize(12);
    executor.setQueueCapacity(200);
    executor.setThreadNamePrefix("receipt-");
    executor.setWaitForTasksToCompleteOnShutdown(true);
    return executor;
}

Size the pool from task behavior, downstream capacity, latency objective, and memory budget. Define rejection and shutdown semantics. If work must survive process crashes or deployment, use a durable broker or database-backed queue instead of in-memory @Async.

16. Common Spring Boot failure scenarios

Failure Evidence to inspect Typical root causes
NoSuchBeanDefinitionException Required type, qualifiers, scan root, conditions, active profiles. Class outside scan, skipped configuration, wrong context, or missing dependency.
NoUniqueBeanDefinitionException Candidate names, primary and qualifier metadata. Multiple strategies without an explicit default or selector.
BeanCurrentlyInCreationException Full dependency path in the cause chain. Circular dependency or reentrant lookup during initialization.
UnsatisfiedDependencyException Deepest cause, not only the outer constructor. Missing candidate, conversion error, or dependency creation failure.
Configuration binding failure Property prefix, origin, target field, validation message. Wrong key, invalid type, missing required value, or profile mismatch.
Web server fails to start Failure analysis, port ownership, connector and TLS configuration. Port in use, bad certificate, unsupported protocol, or missing web stack.
Database startup failure Driver, URL, credentials source, DNS, TLS, pool timeout, migration logs. Wrong profile, missing driver, network policy, secret, or failed migration.
Expected auto-config bean absent Condition evaluation report and classpath. Class condition failed, user bean caused back-off, or property disabled feature.
Annotation appears ignored Runtime bean type, proxy status, call path, processor registration. Direct construction, self-invocation, final/private method, or missing enablement.
Application starts but first request fails Lazy initialization, request path, health design, startup readiness. Misconfigured lazy bean or initialization deferred until traffic.

17. A systematic debugging playbook

  1. Preserve the first failure. Read the entire cause chain and find the deepest actionable exception. Later shutdown warnings are often consequences.
  2. Identify the bean and phase. Was Spring registering metadata, selecting a constructor, binding configuration, populating a dependency, running init code, or applying a post-processor?
  3. Draw the dependency path. Translate nested bean-creation messages into A -> B -> C. The root problem is frequently C.
  4. Inspect configuration origin. Check active profiles and property source origin, not only the value expected from one YAML file.
  5. Inspect conditional decisions. Run with --debug or enable the condition report. Search for the exact auto-configuration class and its matched or unmatched condition.
  6. Verify ownership and call path. Confirm the object came from the context, inspect whether it is proxied, and determine whether the failing call crossed the proxy.
  7. Reduce the context. Reproduce a configuration issue with ApplicationContextRunner, a slice test, or a minimal application.
  8. Compare the effective runtime. Inspect dependency tree, packaged JAR contents, Java version, environment, image digest, and configuration supplied in the failing deployment.
  9. Fix the model. Prefer correcting component boundaries, configuration contracts, or dependency alignment over exclusions and global flags that hide symptoms.
Useful diagnostic commands
# Show the auto-configuration condition report
java -jar app.jar --debug

# Maven runtime dependency tree
./mvnw dependency:tree

# Gradle dependency insight
./gradlew dependencyInsight \
  --dependency spring-core \
  --configuration runtimeClasspath

# Inspect packaged Boot archive entries
jar tf build/libs/app.jar | sort

# Record startup for Java Flight Recorder
java -XX:StartFlightRecording=filename=startup.jfr,duration=60s \
  -jar build/libs/app.jar

Use diagnostics safely

Actuator endpoints such as conditions, beans, mappings, configuration properties, environment, startup, loggers, and thread dump can shorten diagnosis. They also expose architecture, property names, runtime state, and sometimes sensitive values. Keep them disabled or private by default, authorize operators, audit access, and remove temporary exposure after the incident.

18. Performance, security, and production practices

Startup and memory

  • Keep component scans within intentional package roots.
  • Avoid network calls and large data loads in constructors and post-processors.
  • Measure startup steps before enabling broad lazy initialization.
  • Lazy initialization can improve startup time but moves configuration failure and allocation to live requests. Mark critical beans eager and size memory for the fully initialized application.
  • Use current dependency-management releases instead of manually mixing Spring module versions.
  • Test AOT or native deployments separately because reflection and proxy assumptions differ.

Proxy and advice cost

A proxy call has overhead, but database, network, serialization, and logging work normally dominate it. The larger risk is accidentally applying expensive advice to high-volume methods or creating excessive metric dimensions and logs inside advice. Use narrow pointcuts, stable tags, sampling where appropriate, and measurements before optimization.

Container extension security

  • Treat third-party starters and auto-configurations as executable code with access to application classes, configuration, network, filesystem, and secrets.
  • Pin and scan dependencies, verify publishers, and remove unused starters.
  • Never expose the context or a general getBean(String) facility to untrusted input. That creates an object-selection and invocation surface.
  • Do not evaluate untrusted Spring Expression Language. Expression evaluation can expose methods and objects far beyond simple property substitution.
  • Keep dynamic plugin loading behind signatures, isolation, explicit capability contracts, and an operational disable path.
  • Minimize diagnostic endpoint exposure and assume heap dumps and environment reports are sensitive.

Production extension design

An internal starter should publish configuration metadata, use a unique property prefix, validate required configuration, back off when users provide beans, avoid scanning the user's packages, and test every condition branch. It should have no hidden remote startup call and should explain which beans and endpoints it creates.

19. System-design connections

Spring internals matter because framework boundaries often become architecture boundaries.

Spring concept System-design connection Design lesson
Dependency graph Module coupling and ownership Constructor cycles often reveal unclear responsibility.
Proxy boundary Unit of transaction, authorization, caching, or retry Make cross-cutting boundaries explicit at service APIs.
Application event In-process observer pattern It decouples beans, but is not automatically durable or distributed.
Auto-configuration Platform convention and paved road Defaults need back-off, visibility, and safe operational contracts.
ApplicationContext Process boundary It does not coordinate state across replicas or survive process loss.
@Async Local concurrency Use durable messaging for crash survival, replay, and cross-service load leveling.
Singleton scope One instance per context, not global cluster state Never use it as a distributed lock or source of shared truth.
Transaction interceptor Local atomic boundary External effects need idempotency, outbox, saga, or compensation patterns.

Application events versus durable messages

Spring application events are normally in-memory calls within one process. Synchronous listeners can participate in the publisher's call stack and transaction. Async listeners use local executor capacity. Neither form guarantees survival after a crash. When an event represents a business fact that must be delivered, store it transactionally through an outbox and publish it to durable messaging infrastructure.

Context hierarchies

Parent and child contexts can separate shared infrastructure from web-layer beans, but visibility is directional: a child can normally see parent beans, while the parent cannot see child beans. Annotation infrastructure such as transaction or async enablement applies in the context where it is registered. Most Boot services should keep one clear context unless a framework integration genuinely requires a hierarchy.

20. Common mistakes and corrections

Mistake Why it fails Correction
Construct an annotated service with new. It misses injection, lifecycle, proxy, and configuration behavior. Obtain it through injection or explicitly treat it as a plain object.
Put @Transactional on a private helper. Normal proxy advice cannot intercept it. Place the boundary on a proxyable service operation.
Call an advised method through this. The call bypasses the proxy. Extract a collaborating bean with an explicit boundary.
Inject ApplicationContext everywhere. Service locator usage hides dependencies and spreads container coupling. Use constructor injection or a narrow provider for a real dynamic need.
Enable circular references globally. Partially initialized objects and fragile proxy interactions remain. Redesign ownership and orchestration.
Assume annotation presence guarantees behavior. Required processor, advisor, context, or proxy may be absent. Identify the interpreter and test observable behavior.
Use broad pointcuts such as every service method. Unexpected methods gain cost and semantic changes. Select stable annotations or narrow package and method contracts.
Add starters until a missing bean appears. It expands classpath behavior and hides the real condition. Read the condition report and add only the required dependency.
Catch startup exceptions inside init methods. The context becomes ready with broken required capability. Fail fast for required dependencies; model optional degradation explicitly.
Use lazy initialization as a universal startup fix. Failures and resource spikes move to production traffic. Profile startup and optimize the responsible steps.
Assume local events or async work are durable. Process termination loses queued or unpublished work. Use durable messaging and idempotent consumers.
Debug only the outer exception. Bean-creation wrappers hide the actionable cause. Follow the deepest cause and dependency path.

21. Practice exercises

  1. Draw the complete lifecycle of a singleton that uses constructor injection, @PostConstruct, a custom init method, and a transaction proxy. Mark where the raw target and final managed reference differ.
  2. A method annotated @Transactional(REQUIRES_NEW) is called from another method in the same class. Predict the transaction behavior, prove it with an integration test, and refactor the boundary.
  3. Build an ApplicationContextRunner test matrix for an auto-configuration that depends on a class, an enabled property, and a missing user bean.
  4. Diagnose a NoUniqueBeanDefinitionException with three payment gateways. Decide whether @Primary, qualifiers, or a runtime strategy registry models the real need.
  5. Given services A, B, and C where A depends on B, B depends on C, and C depends on A, propose two designs that remove the cycle and explain the new ownership.
  6. Create one JDK-proxied and one CGLIB-proxied service. Inspect their runtime classes, method surfaces, final-method behavior, and self-invocation behavior.
  7. Design advice ordering for authorization, tracing, retry, and transaction behavior. Explain which concern is outermost and whether each retry receives a fresh transaction.
  8. A Boot service starts locally but fails in its container. Build an evidence checklist covering Java version, packaged archive, active profiles, property origins, DNS, secrets, and classpath.
  9. Replace a critical @Async email workflow with a durable design. Include commit atomicity, retry, idempotency, dead-letter handling, ordering, and observability.
  10. Profile startup with condition reporting and application-startup data. Identify one slow bean and choose between redesign, deferral, parallelism, caching, or removal.

22. Comprehensive interview questions and answers

What is the difference between BeanFactory and ApplicationContext?

BeanFactory is the core dependency injection and bean lifecycle contract. ApplicationContext extends the container model with automatic post-processor discovery, events, resources, messages, environment support, and application integrations. Most applications use an application context, while low-level framework code may interact directly with a bean factory.

What is a BeanDefinition?

It is metadata describing how the container should create and manage a bean. It can hold the implementation type or factory, arguments, properties, scope, lazy state, qualifiers, lifecycle methods, and other attributes. It is not the object instance.

What are the main phases of bean creation?

Spring resolves metadata, chooses a construction path, instantiates the raw object, populates dependencies, invokes aware callbacks, runs before-initialization processors, invokes initialization callbacks, runs after-initialization processors, and exposes the final managed reference. On context close it invokes applicable destruction callbacks.

What is the difference between BeanFactoryPostProcessor and BeanPostProcessor?

A factory post-processor changes bean definitions or factory state before ordinary beans are created. A bean post-processor participates in individual bean instance lifecycles and may inspect or replace each object. Configuration parsing is a factory-level concern; annotated injection and auto-proxying are instance-level concerns.

How does @Autowired work internally?

An annotation-aware bean post-processor finds injection points and asks the bean factory to resolve a dependency descriptor. The factory finds type-compatible candidates and applies qualifiers, primary and priority rules, name fallback, generic metadata, and required or optional semantics.

Why is constructor injection preferred?

It makes required dependencies explicit, allows immutable fields, creates objects that are valid after construction, supports plain unit testing, and exposes cycles immediately. Optional or reconfigurable collaborations may use other patterns, but field injection hides dependencies and complicates direct testing.

Why are constructor circular dependencies impossible?

A requires a completed B to construct, while B requires a completed A. Neither raw object exists early enough to supply the other constructor. Property cycles can sometimes use early references, but they expose partial initialization and should be redesigned.

What does a BeanPostProcessor commonly do?

It may invoke lifecycle annotations, inject annotated fields or methods through specialized subinterfaces, validate instances, adapt framework interfaces, or wrap eligible beans with proxies. Much of Spring's annotation behavior is implemented through post-processors.

Why might a bean be ineligible for auto-proxying?

It may have been instantiated while post-processors themselves were being created, before the full chain was registered. A post-processor that directly autowires ordinary application beans can trigger this. Keep infrastructure dependencies minimal and avoid early application-bean creation.

What is the difference between a JDK proxy and CGLIB proxy?

A JDK dynamic proxy implements interfaces and intercepts calls through that interface surface. A CGLIB proxy subclasses the concrete target and overrides proxyable methods. Final classes cannot be subclassed, and final or private methods cannot be advised through class overriding. Both approaches still have the self-invocation limitation in proxy-based AOP.

Why does self-invocation bypass @Transactional?

The external caller enters through a proxy, but a method calling another method on this invokes the target directly. The call never returns to the proxy's transaction interceptor. Extract the inner transactional operation to another managed bean.

What are aspect, pointcut, advice, join point, and proxy?

An aspect groups cross-cutting policy. A join point is a place behavior can apply, primarily method execution in Spring AOP. A pointcut selects join points. Advice is the behavior run before, after, or around them. The proxy is the client-facing object that executes the interceptor chain and then delegates to the target.

How does Spring Boot auto-configuration work?

Boot imports known auto-configuration candidates from library metadata. Conditional annotations evaluate classpath, bean, property, resource, and application facts. Matching configuration adds definitions, while non-matching configuration is skipped. Missing-bean conditions allow user configuration to replace defaults.

Does @ConditionalOnMissingBean check every future bean?

It evaluates against definitions processed when the condition runs. This ordering sensitivity is why it is intended mainly for auto-configuration, which runs after user definitions are available. A condition report and context-runner tests should verify the intended back-off.

What is the difference between @Configuration full and lite mode?

Full mode can enhance the configuration class so direct calls between @Bean methods are redirected through container semantics. Lite mode, including proxyBeanMethods = false, treats them as ordinary factory methods, so a direct Java call creates a new ordinary object. Method-parameter injection works clearly in either mode.

What is FactoryBean?

It is a bean that creates another object exposed as its product. Looking up the normal bean name returns the product, while prefixing the name with & returns the factory. It is useful for complex framework-managed construction.

What is the order of initialization callbacks?

After dependencies and aware callbacks, before-initialization processors run. For distinct callback methods, @PostConstruct is invoked before InitializingBean.afterPropertiesSet(), followed by a configured custom init method. After-initialization processors then receive the result and may return a proxy.

When is ApplicationReadyEvent published?

It is published after the context has refreshed and application runners have completed. It is a late startup milestone, but listeners should still be quick, bounded, and idempotent. Operational readiness must also account for the platform's readiness state and dependencies.

How do you debug why auto-configuration did not apply?

Enable the condition evaluation report, find the exact auto-configuration class, and inspect each matched and unmatched condition. Then verify classpath, property origins, active profiles, user beans that caused back-off, context type, and declared bean return types.

Why can @Async silently run synchronously?

The invocation may be a self-call, the object may not be managed, async support may not be enabled, or the method may not be proxyable. Confirm the injected object is proxied and that the caller crosses that proxy using the intended executor.

Does @Transactional propagate into an @Async thread?

An imperative transaction is normally bound to the calling thread, so it does not propagate to a newly started executor thread. The async method can start its own transaction through its own proxy boundary. Passing managed entities across the boundary also risks detached state and stale assumptions.

How does advice order affect retry and transactions?

If retry advice wraps transaction advice, every attempt can enter a fresh transaction. If transaction advice wraps retry, several attempts may occur inside one transaction that has already been marked rollback-only. Set ordering deliberately and prove it with failure-path tests.

What is the first step when a Boot application fails to start?

Preserve the first failure output and follow the deepest cause chain. Identify the bean and lifecycle phase, draw the dependency path, and use failure analysis and the condition report. Shutdown errors printed later are often secondary.

When should an application use ApplicationContextRunner?

Use it to test focused configuration and auto-configuration combinations quickly. It can vary user beans, properties, classpath, and context type without starting the complete application, making it ideal for testing conditional matching and back-off.

Are Spring application events a message broker?

No. They are in-process publication within an application context. They do not inherently provide durable storage, cross-process delivery, replay, partitioning, or crash recovery. Use a durable broker and an outbox when delivery is a business requirement.

What are the risks of broad lazy initialization?

It delays configuration errors until first use, shifts allocation and latency to live traffic, complicates readiness, and can hide memory needs during startup. Profile the actual slow steps and keep critical capabilities eager.

How would you design a company Spring Boot starter?

Separate dependencies from auto-configuration, use imports metadata and a unique property prefix, validate configuration, guard optional technologies with class conditions, back off for user beans, avoid broad scanning and remote startup work, provide context-runner tests, publish configuration metadata, document created beans, and maintain an operational disable switch.

23. Concise cheat sheet

  • BeanDefinition is a recipe; the managed bean is the resulting reference.
  • ApplicationContext adds events, resources, environment, and processor discovery.
  • Factory post-processors change metadata; bean post-processors change instances.
  • Annotation behavior exists because registered infrastructure interprets the annotation.
  • The final singleton reference may be a proxy rather than the raw target.
  • Calls must cross the proxy for normal Spring AOP advice to run.
  • Self-invocation bypasses @Transactional, @Async, and similar advice.
  • JDK proxies expose interfaces; CGLIB proxies subclass concrete classes.
  • Final and private methods cannot be overridden by class-based proxy advice.
  • Constructor cycles are unresolvable and usually expose confused ownership.
  • Do not depend on early references or globally enable circular dependencies.
  • Use method parameters between @Bean methods for clear dependency resolution.
  • Auto-configuration imports known candidates and keeps only condition matches.
  • Missing-bean conditions implement default back-off for user configuration.
  • The condition report explains why auto-configuration matched or did not match.
  • Use ApplicationContextRunner to test condition and back-off matrices.
  • Startup listeners must be registered before the events they need to observe.
  • Imperative transactions and thread-local context do not automatically cross async threads.
  • Advice ordering changes semantics, especially for retry and transactions.
  • Read the deepest startup cause and map the dependency path before changing configuration.
  • Profile startup before using broad lazy initialization.
  • Local events and executors are not durable distributed infrastructure.
  • Treat starters, diagnostics, SpEL, and dynamic extensions as security surfaces.

24. Official references

Last reviewed · July 2026 · part of knowledge-base