Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Spring Boot Quickstart
Spring Boot is an opinionated framework that simplifies the creation of stand-alone, production-grade Spring-based applications. It takes the “convention over configuration” approach. Real-world analogy: Think of plain Spring Framework as buying raw lumber, nails, and blueprints to build a house from scratch. Spring Boot is like buying a prefab home — the walls are pre-assembled, the plumbing is routed, and the electrical wiring follows code. You can still customize everything, but the 80% of decisions that are the same for every house are already made for you. That is what “opinionated defaults” means in practice.1. The Magic of Spring Initializr
You rarely start a Spring project from scratch. You use the Spring Initializr.- Go to start.spring.io
- Project: Maven / Gradle (We’ll use Maven for this course)
- Language: Java
- Spring Boot: 3.x.x (Latest stable)
- Project Metadata:
- Group:
com.devweekends - Artifact:
demo - Packaging:
Jar - Java: 17
- Group:
- Dependencies (Add these):
- Spring Web: For building REST APIs (uses Tomcat as default embedded server).
- Spring Boot DevTools: For fast feedback loops (auto-restart).
- Lombok: To reduce boilerplate code.
2. Project Structure
The Entry Point
@SpringBootApplication annotation is a convenience annotation that adds all of the following:
@Configuration: Tags the class as a source of bean definitions.@EnableAutoConfiguration: Tells Spring Boot to start adding beans based on classpath settings (e.g., ifspring-webis on the classpath, setup Tomcat).@ComponentScan: Tells Spring to look for other components, configurations, and services in thecom/devweekends/demopackage.
3. Your First REST Controller
Create a new fileHelloController.java next to DemoApplication.java.
@RestController: Marks this class as a request handler where every method returns a domain object directly (JSON/XML) instead of a view.@GetMapping("/hello"): Maps HTTP GET requests to/helloto thesayHello()method.
4. Running the Application
In your terminal (or IDE):Tomcat started on port(s): 8080 (http)
Open http://localhost:8080/hello in your browser. You should see “Hello, Dev Weekends!“.
5. Dependency Injection (DI) Basics
Spring’s core is the Application Context (the IoC container). It manages the lifecycle of your objects (Beans). The Factory Analogy: Imagine a car assembly plant (the IoC container). Instead of each worker going out to buy their own screws, engines, and tires, the factory maintains a warehouse of parts (beans) and delivers exactly what each workstation needs. The workers (your classes) declare what they need (“I need an engine”), and the factory (Spring) delivers it. This is Inversion of Control — your code does not create its own dependencies; the container “inverts” that responsibility and hands them to you. Instead ofnew Service(), you ask Spring to give you an instance.
new HelloController(mockGreetingService) — no Spring context needed.
6. Spring Boot Internals: How it Works
Many developers use@SpringBootApplication without knowing what it does. It’s a compound annotation:
@SpringBootConfiguration: Just a specialized@Configuration.@ComponentScan: Scans the current package and sub-packages for Components.@EnableAutoConfiguration: The Magic.
How Auto-Configuration Works
Spring Boot looks at the classpath and makes intelligent decisions based on what it finds — like a smart home system that detects which appliances are plugged in and configures itself accordingly.- Is
spring-webmvcon the classpath? -> Configure DispatcherServlet and embedded Tomcat. - Is
h2on the classpath? -> Configure DataSource with an in-memory database. - Is
hibernate-coreon the classpath? -> Configure EntityManagerFactory with sensible defaults.
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports to find configuration classes and applies them conditionally (@ConditionalOnClass, @ConditionalOnMissingBean).
Production tip: Auto-configuration is powerful but can surprise you. If Spring configures something you did not expect, add --debug to your startup args or set debug=true in application.properties. This prints a detailed Conditions Evaluation Report showing every auto-configuration class that was matched or skipped, and why. This is your go-to diagnostic tool when Spring “magically” does something you did not ask for.
7. Bean Scopes & Lifecycle
By default, all beans are Singletons (created once per app). But you can change this.Scopes
- Singleton (Default): One instance per container. Think of it as a shared office printer — everyone uses the same one.
- Prototype: A new instance every single time it is requested. Like disposable coffee cups — fresh one per use.
- Request: One instance per HTTP Request. Like a shopping cart that exists only for the duration of a single API call.
- Session: One instance per HTTP Session. Like a user’s browser tab state — persists across multiple requests from the same session.
ObjectFactory<T> or Provider<T> instead, and call .getObject() / .get() when you need a new instance.
Lifecycle Callbacks
Sometimes you need to run logic right after a bean is created (e.g., open a socket) or before it dies (e.g., close file).8. Type-Safe Configuration
Stop using@Value("${my.config}") for anything beyond trivial cases. Scattered @Value annotations are the configuration equivalent of magic numbers — hard to find, impossible to validate at startup, and easy to typo. Instead, group related properties into a POJO.
application.yml
MailConfig anywhere. The major advantage: Spring validates the entire config block at startup. If app.mail.port is missing or cannot be parsed as an integer, the application fails fast with a clear error message instead of crashing at 3 AM when the code path is first hit.
Production tip: Add @Validated and Jakarta validation annotations to your config class for even stricter startup checks:
9. Deep Dive: The IoC Container
The Inversion of Control (IoC) container is the heart of Spring. It manages the lifecycle of your objects (Beans).BeanFactory vs ApplicationContext
- BeanFactory: The root interface. Provides basic features (DI). Lazy loading.
- ApplicationContext: A sub-interface. Adds enterprise specific functionality:
- Internationalization (i18n).
- Event publishing.
- Web application support.
- Eager loading (instantiates singletons on startup), which is better for detecting errors early.
In Spring Boot,SpringApplication.run()returns anApplicationContext.
10. Dependency Injection Patterns
How should you inject dependencies?1. Field Injection (Avoid)
- Pros: Concise.
- Cons: Hides dependencies. Impossible to unit test without reflection/mocks. Creates circular dependency risks.
2. Setter Injection (Optional Deps)
- Pros: Allows optional dependencies (can re-configure later).
- Cons: Bean is not immutable.
3. Constructor Injection (Recommended)
- Pros:
- Immutability: Fields can be
final. - Testability: Just pass mocks in
new UserController(mockService). - Safety: Object is fully initialized before use.
- Circular Dependency Detection: Fails fast at startup if A -> B -> A.
- Immutability: Fields can be
multiple Implementations? Use @Qualifier or @Primary
If you haveSmsNotificationService and EmailNotificationService implementing NotificationService, Spring gets confused.
@Primary: The default choice.@Qualifier("sms"): Specific choice injecting time.
11. Advanced Bean Lifecycle
It’s not just “Create -> Use -> Destroy”.BeanPostProcessors (BPP)
These are hooks that allow you to modify beans before they are fully initialized. Example: How@Transactional works.
Spring has a BPP that scans for @Transactional. If found, it wraps your bean in a Proxy (CGLIB or JDK Dynamic Proxy) before handing it to the container. The container never holds your actual class, only the Proxy!
Interview Deep-Dive
Explain the difference between BeanFactory and ApplicationContext. When would you ever use BeanFactory directly?
Explain the difference between BeanFactory and ApplicationContext. When would you ever use BeanFactory directly?
BeanFactoryis the root IoC container interface. It provides the core DI mechanism: bean instantiation, wiring, and lifecycle management. Critically, it uses lazy initialization — beans are not created until they are first requested viagetBean(). This means startup is fast, but you will not discover configuration errors (missing dependencies, circular references) until runtime when that code path is actually hit.ApplicationContextextendsBeanFactoryand adds enterprise features: event publishing (ApplicationEventPublisher), internationalization (MessageSource), environment abstraction, and — most importantly for production — eager initialization of singleton beans. This means all your@Service,@Repository, and@Controllerbeans are created at startup. If you have a typo in a@Qualifieror a missing dependency, the app fails to start rather than failing at 2 AM when a rarely-used code path executes.- In practice, you almost never use
BeanFactorydirectly. The one legitimate use case is in extremely memory-constrained environments (embedded systems, certain Android scenarios) where you cannot afford to eagerly instantiate hundreds of beans. Spring Boot always usesApplicationContext— specificallyAnnotationConfigServletWebServerApplicationContextfor servlet-based web apps andAnnotationConfigReactiveWebServerApplicationContextfor WebFlux apps. - The gotcha that catches people: even within an
ApplicationContext, prototype-scoped beans are still lazy. The container creates them on demand and does not manage their full lifecycle (no@PreDestroycallback for prototypes). This is a common source of resource leaks.
@Lazy on the bean definition or on the injection point. When placed on a @Component class, Spring defers creation until first access. When placed on a constructor parameter (@Lazy UserService userService), Spring injects a proxy that triggers real initialization on first method call. This is useful for breaking circular dependencies or deferring expensive initialization (like a bean that opens a connection pool to a rarely-used legacy database). But be careful: @Lazy on an injection point means your startup validation no longer covers that dependency chain. If the lazy bean has a misconfiguration, you will only discover it at runtime.Why does Spring recommend constructor injection over field injection? Go beyond the testing argument -- what are the deeper implications?
Why does Spring recommend constructor injection over field injection? Go beyond the testing argument -- what are the deeper implications?
- The testing argument is the one everyone knows: with constructor injection, you can write
new OrderService(mockRepo, mockClient)in a unit test without any Spring context or reflection hacks. With field injection (@Autowired private OrderRepository repo), you need Mockito’s@InjectMocksor Spring’s test context, both of which are slower and more fragile. - But the deeper reason is about invariant enforcement. Constructor injection lets you declare fields as
final, which means the dependency graph of a bean is immutable after construction. The object is either fully initialized or it does not exist. With field injection, there is a window between object construction and field population where the object exists in an invalid state — all@Autowiredfields are null. If any initialization code in@PostConstructor in the constructor itself tries to use those fields, you get aNullPointerException. - Constructor injection also makes circular dependencies fail fast. If Service A needs Service B in its constructor, and Service B needs Service A in its constructor, Spring cannot create either and throws
BeanCurrentlyInCreationExceptionat startup. With field injection, Spring can create both objects (fields are null), then populate fields via reflection, silently hiding the circular dependency. That circular dependency is still a design problem — you just do not discover it until it causes subtle ordering bugs in production. - There is also a design pressure benefit: if your constructor has 12 parameters, that is a code smell screaming “this class has too many responsibilities.” Field injection hides this because adding
@Autowired private AnotherService xis one silent line. Constructor injection forces you to confront it.
@RequiredArgsConstructor to keep it concise — make all injected fields private final, add the annotation, and delete the @Autowired field annotations. Run the tests after each class migration to catch any circular dependency that was previously hidden. Over 2-3 months, the hot paths are migrated organically. For an automated approach, tools like OpenRewrite have recipes that can do this refactoring across an entire codebase in one PR, but I would still review the diff carefully for circular dependency breakage.A junior developer writes a Prototype-scoped bean and injects it into a Singleton service. They expect a fresh instance per request, but it does not work. Explain what went wrong at the container level and how to fix it.
A junior developer writes a Prototype-scoped bean and injects it into a Singleton service. They expect a fresh instance per request, but it does not work. Explain what went wrong at the container level and how to fix it.
- Here is what happens internally: Spring creates the singleton
OrderServiceonce during startup. During construction, it resolves all dependencies, including the prototype-scopedShoppingCart. Spring creates oneShoppingCartinstance and injects it. From this point forward,OrderServiceholds a direct reference to that singleShoppingCartinstance. When another request comes in,OrderServiceis already fully constructed — Spring does not re-inject its fields. The prototype scope promise (“new instance every time it is requested from the container”) is honored, but nobody is requesting it from the container again. The singleton just uses its stale reference. - Fix 1: Inject
ObjectFactory<ShoppingCart>orProvider<ShoppingCart>(fromjakarta.inject). Each time you callprovider.get(), Spring goes back to the container and creates a fresh prototype instance. This is the cleanest approach for most use cases. - Fix 2: Use
@Lookupmethod injection. Declare an abstract method or a method that Spring will override at runtime via CGLIB to return a fresh prototype instance. Less common in modern code but still valid. - Fix 3: Inject
ApplicationContextdirectly and callcontext.getBean(ShoppingCart.class). This works but is the Service Locator anti-pattern — it couples your code to the Spring API and makes testing harder. - Fix 4: For web applications specifically, use
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)instead of prototype. This creates a CGLIB proxy that delegates to a request-scoped instance. The singleton holds the proxy, and each HTTP request gets its own real instance behind the proxy. This is typically what people actually want when they think “prototype per request.”
@PostConstruct), hands it off, and then forgets about it. @PreDestroy is never called by the container for prototype beans because the container does not track them after creation. If your prototype bean holds a resource (a file handle, a socket, a database connection), you are responsible for closing it yourself. This is documented but rarely read, and it leads to resource leaks in production. If you need lifecycle management for short-lived beans, consider request scope with a proxy — Spring does manage the full lifecycle for request-scoped beans.Explain how Spring Boot auto-configuration actually works. Specifically, what role do @Conditional annotations play, and how would you write a custom auto-configuration for an internal library?
Explain how Spring Boot auto-configuration actually works. Specifically, what role do @Conditional annotations play, and how would you write a custom auto-configuration for an internal library?
- Auto-configuration is not magic — it is conditional bean registration at scale. Spring Boot ships with hundreds of
@Configurationclasses (likeDataSourceAutoConfiguration,WebMvcAutoConfiguration), each registered inMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. At startup, Spring loads all of these classes and evaluates their conditions. - The
@Conditionalfamily is the gating mechanism.@ConditionalOnClass(DataSource.class)checks if that class exists on the classpath (without loading it).@ConditionalOnMissingBean(DataSource.class)checks if the user has already defined their ownDataSourcebean.@ConditionalOnProperty(name = "app.feature.enabled", havingValue = "true")checks a configuration property. These compose: a configuration class might require all three conditions to be true before its beans are registered. - The evaluation order matters and is controlled by
@AutoConfigureOrder,@AutoConfigureBefore, and@AutoConfigureAfter. For example,DataSourceAutoConfigurationmust run beforeJpaRepositoriesAutoConfigurationbecause JPA needs a DataSource bean to already exist. - To write a custom auto-configuration for an internal library: (1) Create a
@Configurationclass with@ConditionalOnClassfor your library’s key class. (2) Define beans with@ConditionalOnMissingBeanso users can override them. (3) Use@ConfigurationPropertiesfor externalized defaults. (4) Register it inMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsin your library’s JAR. (5) Crucially, put the configuration in a separate Maven module from the library itself — theautoconfiguremodule depends on the library, and thestartermodule depends on both. This is the same structure Spring Boot itself uses.
debug=true in application.properties or passing --debug at startup. The report will list every auto-configuration class under “Positive matches” (applied) or “Negative matches” (skipped) with the exact condition that failed. Common failures: the class referenced in @ConditionalOnClass is not on the classpath (missing dependency), a @ConditionalOnMissingBean matched because another configuration already defined that bean type, or the imports file has a typo in the fully qualified class name. Also check the @AutoConfigureAfter ordering — if your auto-config depends on another that has not run yet, the @ConditionalOnBean check might fail because the bean it is looking for does not exist yet at that point in the configuration phase.Describe the full lifecycle of a Spring Bean from instantiation to destruction. Where exactly do BeanPostProcessors fit in, and why does that matter for understanding @Transactional and @Async?
Describe the full lifecycle of a Spring Bean from instantiation to destruction. Where exactly do BeanPostProcessors fit in, and why does that matter for understanding @Transactional and @Async?
- The lifecycle has distinct phases. First, Spring instantiates the bean via the constructor (this is where constructor injection happens). Second, it populates properties (setter injection,
@Valueresolution). Third, it calls Aware interfaces (BeanNameAware,BeanFactoryAware,ApplicationContextAware) so the bean can learn about its environment. Fourth — and this is the critical phase — it runsBeanPostProcessor.postProcessBeforeInitialization()on every registered BPP. Fifth, it calls initialization callbacks (@PostConstruct,InitializingBean.afterPropertiesSet(), custominit-method). Sixth, it runsBeanPostProcessor.postProcessAfterInitialization(). Seventh, the bean is ready for use. On shutdown, it calls@PreDestroy,DisposableBean.destroy(), and customdestroy-method. - The
postProcessAfterInitializationphase is where the proxy wrapping happens.AnnotationAwareAspectJAutoProxyCreator(a BPP) inspects every bean after initialization. If the bean or any of its methods are annotated with@Transactional,@Async,@Cacheable, or matched by any AOP pointcut, the BPP replaces the original bean instance with a CGLIB proxy (or JDK dynamic proxy if it implements an interface). The ApplicationContext then stores the proxy, not your original object. - This is why self-invocation breaks
@Transactional. When your method callsthis.otherMethod(),thisis the raw target object inside the proxy, not the proxy itself. The call bypasses the proxy’s transaction interceptor entirely. The transaction advice is a method interceptor on the proxy — if you do not go through the proxy, the advice never fires. - This also explains why
@Transactionalon aprivatemethod has no effect. CGLIB creates a subclass of your bean, and it cannot override private methods. The proxy method just callssuper.yourMethod()without wrapping it in transaction logic. Spring does not warn you about this by default — your@Transactional(readOnly = true)on a private helper method is silently ignored.
AopUtils.isAopProxy(bean), AopUtils.isCglibProxy(bean), or AopUtils.isJdkDynamicProxy(bean). To get the underlying target from a proxy, use AopProxyUtils.getSingletonTarget(bean) or cast to Advised and call getTargetSource().getTarget(). In debugging, a quick way to tell is that a CGLIB proxy’s getClass().getName() will contain $$SpringCGLIB$$ in it. If you are debugging why a @Transactional annotation is not being honored, the first thing to check is whether the bean injected into the caller is actually a proxy. If it is not, the BPP did not wrap it, which means either the annotation is on a private method, or the class is being instantiated manually with new instead of being managed by the container.