Skip to main content

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.
  1. Go to start.spring.io
  2. Project: Maven / Gradle (We’ll use Maven for this course)
  3. Language: Java
  4. Spring Boot: 3.x.x (Latest stable)
  5. Project Metadata:
    • Group: com.devweekends
    • Artifact: demo
    • Packaging: Jar
    • Java: 17
  6. 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.
Click GENERATE, unzip the project, and open it in your IDE (IntelliJ or VS Code).

2. Project Structure

src/
├── main/
│   ├── java/
│   │   └── com/devweekends/demo/
│   │       └── DemoApplication.java  <-- Entry Point
│   └── resources/
│       ├── application.properties    <-- Configuration
│       ├── static/                   <-- Static assets (JS/CSS)
│       └── templates/                <-- Server-side templates (Thymeleaf)

The Entry Point

package com.devweekends.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// This single annotation replaces 3 separate annotations (see below).
// SpringApplication.run() bootstraps the IoC container, starts the embedded
// server, and triggers auto-configuration -- all in one line.
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        // This returns an ApplicationContext -- the "factory" that holds all your beans.
        // In production, you rarely interact with this directly, but knowing it exists
        // helps you understand how Spring manages your entire object graph.
        SpringApplication.run(DemoApplication.class, args);
    }
}
The @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., if spring-web is on the classpath, setup Tomcat).
  • @ComponentScan: Tells Spring to look for other components, configurations, and services in the com/devweekends/demo package.

3. Your First REST Controller

Create a new file HelloController.java next to DemoApplication.java.
package com.devweekends.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String sayHello() {
        return "Hello, Dev Weekends!";
    }
}
  • @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 /hello to the sayHello() method.

4. Running the Application

In your terminal (or IDE):
./mvnw spring-boot:run
You should see logs starting with the Spring banner. Look for: 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 of new Service(), you ask Spring to give you an instance.
@Service // 1. Register this class as a Bean (tells Spring: "put this in the warehouse")
public class GreetingService {
    public String getGreeting() {
        return "Welcome to Spring Boot Mastery";
    }
}

@RestController
public class HelloController {

    // 'final' ensures immutability -- once Spring injects the service, it cannot be swapped.
    // This is a deliberate design choice: your controller's dependencies are fixed at construction time.
    private final GreetingService greetingService;

    // 2. Constructor Injection (Recommended)
    // Spring sees this constructor, checks its warehouse for a GreetingService bean,
    // and passes it in automatically. No @Autowired needed when there's only one constructor.
    public HelloController(GreetingService greetingService) {
        this.greetingService = greetingService;
    }

    @GetMapping("/greet")
    public String greet() {
        return greetingService.getGreeting();
    }
}
This makes your code testable and loosely coupled. In a unit test, you can simply write 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:
  1. @SpringBootConfiguration: Just a specialized @Configuration.
  2. @ComponentScan: Scans the current package and sub-packages for Components.
  3. @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-webmvc on the classpath? -> Configure DispatcherServlet and embedded Tomcat.
  • Is h2 on the classpath? -> Configure DataSource with an in-memory database.
  • Is hibernate-core on the classpath? -> Configure EntityManagerFactory with sensible defaults.
It uses 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.
Production pitfall: Injecting a Prototype-scoped bean into a Singleton creates a subtle bug. The Singleton is created once, so it gets one Prototype instance and holds onto it forever — defeating the purpose. If you need a fresh Prototype each time, inject an ObjectFactory<T> or Provider<T> instead, and call .getObject() / .get() when you need a new instance.
@Component
@Scope("prototype")
public class UserAction {
    // New instance created every time
}

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).
@Component
public class DatabaseConnection {

    @PostConstruct
    public void init() {
        System.out.println("Bean created! Opening connection...");
    }

    @PreDestroy
    public void cleanup() {
        System.out.println("App shutting down! Closing connection...");
    }
}

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
app:
  mail:
    host: smtp.gmail.com
    port: 587
    enabled: true
Java Config Class
// Spring maps 'app.mail.host' -> host, 'app.mail.port' -> port automatically.
// Using a record gives you immutability for free -- config values cannot be
// accidentally mutated at runtime.
@ConfigurationProperties(prefix = "app.mail")
public record MailConfig(String host, int port, boolean enabled) {}
Enable it in Main Class
@SpringBootApplication
@EnableConfigurationProperties(MailConfig.class)
public class DemoApplication {}
Now you can inject 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:
@Validated
@ConfigurationProperties(prefix = "app.mail")
public record MailConfig(
    @NotBlank String host,
    @Min(1) @Max(65535) int port,
    boolean enabled
) {}

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 an ApplicationContext.

10. Dependency Injection Patterns

How should you inject dependencies?

1. Field Injection (Avoid)

@Autowired
private UserService userService;
  • Pros: Concise.
  • Cons: Hides dependencies. Impossible to unit test without reflection/mocks. Creates circular dependency risks.

2. Setter Injection (Optional Deps)

@Autowired
public void setUserService(UserService userService) {
    this.userService = userService;
}
  • Pros: Allows optional dependencies (can re-configure later).
  • Cons: Bean is not immutable.
private final UserService userService;

public UserController(UserService userService) {
    this.userService = userService;
}
  • 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.

multiple Implementations? Use @Qualifier or @Primary

If you have SmsNotificationService and EmailNotificationService implementing NotificationService, Spring gets confused.
  • @Primary: The default choice.
  • @Qualifier("sms"): Specific choice injecting time.
public class CheckoutService {
    public CheckoutService(@Qualifier("sms") NotificationService sender) { ... }
}

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

Strong Answer:
  • BeanFactory is 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 via getBean(). 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.
  • ApplicationContext extends BeanFactory and 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 @Controller beans are created at startup. If you have a typo in a @Qualifier or 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 BeanFactory directly. 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 uses ApplicationContext — specifically AnnotationConfigServletWebServerApplicationContext for servlet-based web apps and AnnotationConfigReactiveWebServerApplicationContext for 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 @PreDestroy callback for prototypes). This is a common source of resource leaks.
Follow-up: If ApplicationContext eagerly initializes all singletons, how do you make a specific bean lazy without switching the entire container to BeanFactory?Use @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.
Strong Answer:
  • 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 @InjectMocks or 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 @Autowired fields are null. If any initialization code in @PostConstruct or in the constructor itself tries to use those fields, you get a NullPointerException.
  • 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 BeanCurrentlyInCreationException at 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 x is one silent line. Constructor injection forces you to confront it.
Follow-up: You inherit a codebase with field injection everywhere. What is your migration strategy?I would not do a big-bang rewrite. Instead, I would set a team convention: all new code uses constructor injection, and when you touch an existing class for a feature change, you migrate that class. Use Lombok’s @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.
Strong Answer:
  • Here is what happens internally: Spring creates the singleton OrderService once during startup. During construction, it resolves all dependencies, including the prototype-scoped ShoppingCart. Spring creates one ShoppingCart instance and injects it. From this point forward, OrderService holds a direct reference to that single ShoppingCart instance. When another request comes in, OrderService is 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> or Provider<ShoppingCart> (from jakarta.inject). Each time you call provider.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 @Lookup method 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 ApplicationContext directly and call context.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.”
Follow-up: What happens to the lifecycle callbacks (@PreDestroy) of prototype-scoped beans?This is the part most people miss: Spring does not manage the complete lifecycle of prototype beans. The container creates and configures the prototype bean (including calling @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.
Strong Answer:
  • Auto-configuration is not magic — it is conditional bean registration at scale. Spring Boot ships with hundreds of @Configuration classes (like DataSourceAutoConfiguration, WebMvcAutoConfiguration), each registered in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. At startup, Spring loads all of these classes and evaluates their conditions.
  • The @Conditional family 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 own DataSource bean. @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, DataSourceAutoConfiguration must run before JpaRepositoriesAutoConfiguration because JPA needs a DataSource bean to already exist.
  • To write a custom auto-configuration for an internal library: (1) Create a @Configuration class with @ConditionalOnClass for your library’s key class. (2) Define beans with @ConditionalOnMissingBean so users can override them. (3) Use @ConfigurationProperties for externalized defaults. (4) Register it in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports in your library’s JAR. (5) Crucially, put the configuration in a separate Maven module from the library itself — the autoconfigure module depends on the library, and the starter module depends on both. This is the same structure Spring Boot itself uses.
Follow-up: A team member’s auto-configuration is silently not being applied. How do you debug it?First, enable the Conditions Evaluation Report by setting 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.
Strong Answer:
  • 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, @Value resolution). Third, it calls Aware interfaces (BeanNameAware, BeanFactoryAware, ApplicationContextAware) so the bean can learn about its environment. Fourth — and this is the critical phase — it runs BeanPostProcessor.postProcessBeforeInitialization() on every registered BPP. Fifth, it calls initialization callbacks (@PostConstruct, InitializingBean.afterPropertiesSet(), custom init-method). Sixth, it runs BeanPostProcessor.postProcessAfterInitialization(). Seventh, the bean is ready for use. On shutdown, it calls @PreDestroy, DisposableBean.destroy(), and custom destroy-method.
  • The postProcessAfterInitialization phase 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 calls this.otherMethod(), this is 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 @Transactional on a private method has no effect. CGLIB creates a subclass of your bean, and it cannot override private methods. The proxy method just calls super.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.
Follow-up: How would you detect at runtime whether the bean you are holding is a proxy or the actual target?You can check with 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.