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 |
- Is this object actually managed by the same application context?
- Is the caller invoking the object through the Spring-provided reference or directly?
- 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 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.
@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.
@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.
- Resolve merged definition: combine parent metadata, scope, factory information, and definition attributes.
- Choose construction path: select a constructor, factory method, supplier, or factory bean.
- Instantiate: allocate the raw object. Advanced post-processors can participate before or during constructor selection.
- Populate dependencies: resolve constructor arguments during construction and inject annotated fields, methods, or configured properties at the appropriate processor phase.
- Invoke aware callbacks: supply container infrastructure such as bean name, factory, class loader, environment, or application context when explicitly requested.
- Run before-initialization processors: processors may invoke lifecycle annotations or adapt state before init methods.
-
Run initialization callbacks: normally
@PostConstruct, thenInitializingBean.afterPropertiesSet(), then a custom init method when these are different methods. - Run after-initialization processors: processors may return the object or replace it with a proxy.
- Publish managed reference: cache the completed singleton and return the final reference to callers.
-
Destroy on context close: normally
@PreDestroy, thenDisposableBean.destroy(), then a custom destroy method when applicable.
@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.
@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;
}
}
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.
@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.
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. |
@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.
@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.
@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.
@Asynccannot be relied on in the same bean's lifecycle callback.- Private methods cannot be intercepted by the normal proxy mechanism.
-
voidmethods cannot deliver exceptions to the caller. Configure anAsyncUncaughtExceptionHandleror returnCompletableFuture. - 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.
@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
- Preserve the first failure. Read the entire cause chain and find the deepest actionable exception. Later shutdown warnings are often consequences.
- 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?
-
Draw the dependency path. Translate nested bean-creation messages into
A -> B -> C. The root problem is frequently C. - Inspect configuration origin. Check active profiles and property source origin, not only the value expected from one YAML file.
-
Inspect conditional decisions. Run with
--debugor enable the condition report. Search for the exact auto-configuration class and its matched or unmatched condition. - 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.
-
Reduce the context. Reproduce a configuration issue with
ApplicationContextRunner, a slice test, or a minimal application. - Compare the effective runtime. Inspect dependency tree, packaged JAR contents, Java version, environment, image digest, and configuration supplied in the failing deployment.
- Fix the model. Prefer correcting component boundaries, configuration contracts, or dependency alignment over exclusions and global flags that hide symptoms.
# 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
-
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. -
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. -
Build an
ApplicationContextRunnertest matrix for an auto-configuration that depends on a class, an enabled property, and a missing user bean. -
Diagnose a
NoUniqueBeanDefinitionExceptionwith three payment gateways. Decide whether@Primary, qualifiers, or a runtime strategy registry models the real need. - 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.
- Create one JDK-proxied and one CGLIB-proxied service. Inspect their runtime classes, method surfaces, final-method behavior, and self-invocation behavior.
- Design advice ordering for authorization, tracing, retry, and transaction behavior. Explain which concern is outermost and whether each retry receives a fresh transaction.
- 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.
-
Replace a critical
@Asyncemail workflow with a durable design. Include commit atomicity, retry, idempotency, dead-letter handling, ordering, and observability. - 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
BeanDefinitionis a recipe; the managed bean is the resulting reference.-
ApplicationContextadds 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
@Beanmethods 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
ApplicationContextRunnerto 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
- Spring Framework bean definitions
- Spring Framework dependency resolution and circular dependencies
- Spring Framework container extension points
- Spring Framework lifecycle callbacks
- Spring Framework autowired dependencies
- Spring Framework @Bean and @Configuration concepts
- Spring AOP concepts
- Spring AOP proxying mechanisms
- Spring Framework declarative transaction annotations
- Spring Framework task execution and @Async
- Spring Boot auto-configuration
- Creating Spring Boot auto-configuration
- Spring Boot application startup, events, and startup tracking
- Spring Boot Actuator diagnostic endpoints