Spring Boot Core Concepts Tutorial

In simple terms: Spring provides the building blocks; Spring Boot assembles common pieces so you can focus on your application.

What is Spring Boot?

Spring Boot is an opinionated framework built on top of the Spring Framework. It helps developers create production-ready Java applications quickly by choosing sensible defaults and reducing the amount of configuration they must write.

Opinionated does not mean that Spring Boot removes your choices. It means Spring Boot provides a good default first, while still allowing you to change the configuration when your application needs something different.

Why was Spring Boot created?

Before Spring Boot, starting a Spring application often meant configuring many separate pieces manually:

  • web.xml and the DispatcherServlet
  • A servlet container such as Tomcat
  • Maven or Gradle dependencies and compatible versions
  • The Spring application context and bean configuration
  • Property files and environment-specific settings

This setup could require hundreds of lines of configuration before the first business feature was written.

Traditional Spring vs Spring Boot
Traditional SpringConfigure server, context, beans, dependencies, and properties manually
Spring BootAdd starters, use defaults, and run the application
Ready applicationFocus on business features

Minimal Spring Boot application

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

With this class and the right starter dependency, Spring Boot can start the application context and an embedded web server. You can run it like a normal Java program, without separately installing and deploying a WAR file to Tomcat.

Key advantages

Advantage What it means
Embedded Tomcat The server can run inside your executable JAR.
Auto-configuration Spring Boot configures common components when their dependencies are present.
Starter dependencies One dependency brings a compatible group of libraries.
Less XML Java configuration, annotations, and properties replace most manual XML setup.
Production features Health checks, metrics, external configuration, and logging support are available.
Easy deployment Build one JAR and run it with java -jar.

Real-world analogy: opening a restaurant

Imagine opening a restaurant.

Restaurant analogy
Traditional SpringYou arrange tables, chairs, kitchen equipment, gas, electricity, menu, and chef separately
Spring BootThe restaurant infrastructure is ready; you focus on cooking the food

Spring Boot prepares the common infrastructure. You still decide what your application does, how its business rules work, and which features your users receive.

Spring vs Spring Boot

Spring is a large ecosystem for building Java applications. Traditionally, developers configured many objects and framework integrations by hand. Spring Boot adds conventions, starter dependencies, embedded servers, and automatic configuration to reduce that setup work.

What happens when a Boot app starts
main()Start the application
SpringApplicationCreates the context
Auto-configurationFinds useful defaults
Embedded serverServes the application

Minimal application

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

@SpringBootApplication combines component scanning, auto-configuration, and a configuration class. SpringApplication.run() creates the application context and starts the embedded server when web dependencies are present.

Key ideas

  • Convention over configuration: common choices work with little code.
  • Starters: dependency bundles for web, data, security, and testing.
  • Embedded server: run the app as a normal Java process or JAR.
  • External configuration: change environment-specific values without recompiling.

Auto-Configuration

Auto-configuration is Spring Boot's way of choosing common beans for you. Boot checks the libraries on the classpath, your configuration properties, and the beans you already defined. It creates sensible defaults only when they are needed.

How auto-configuration makes a decision
ClasspathIs Spring MVC present?
ConditionsIs a matching bean missing?
Default beanConfigure it automatically

For example, when the web starter is present, Boot can configure an embedded server, Spring MVC infrastructure, JSON support, and a dispatcher servlet. If you define your own bean, Boot usually backs away so your explicit choice wins.

Remember: Auto-configuration is a collection of conditional configuration classes that can be inspected, overridden, or disabled when necessary.

Including dependencies to activate auto-configuration

Auto-configuration is driven by the dependencies in your project. Adding a starter places the required libraries on the classpath, so Spring Boot can detect the feature and configure it.

<!-- Adds Spring MVC, Jackson, validation support, and embedded Tomcat -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Adds JPA, Hibernate, transactions, and repository support -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

After these dependencies are available, Boot checks their classes and creates the matching infrastructure. A dependency does not automatically create a useful application feature by itself; your controllers, entities, repositories, and properties still need to be provided.

Excluding an auto-configuration class

Sometimes a dependency is present but your application does not need the feature it normally activates. You can exclude the related auto-configuration class.

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

The same exclusion can be configured without changing the Java class:

# application.properties
spring.autoconfigure.exclude=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

This is useful for a service that includes a database-related library transitively but does not connect to a database. Exclude only what you understand; removing required auto-configuration can cause missing beans or startup errors.

Excluding an individual dependency

Dependency exclusion is different from auto-configuration exclusion. A dependency exclusion prevents a library from being downloaded or used, while an auto-configuration exclusion keeps the library but prevents Boot from creating its default beans.

<!-- Keep Spring MVC, but remove embedded Tomcat -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Replacing embedded Tomcat with Jetty

Spring Boot's web starter uses embedded Tomcat by default. To use Jetty instead, exclude the Tomcat starter and add the Jetty starter. Do not keep both servlet containers in the same application because the server configuration can become ambiguous.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- Add the alternative embedded servlet container -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
Changing the embedded servlet container
spring-boot-starter-webTomcat by default
Exclude TomcatRemove the default server
Add JettyRun the same MVC app on Jetty

Your controllers and REST endpoints usually do not change because Spring MVC sits above the servlet container. The main difference is the embedded server that receives HTTP requests and starts the application.

Useful auto-configuration controls

Requirement Approach Example
Add a feature Add a starter or library spring-boot-starter-data-jpa
Change a default value Set a property server.port=8081
Replace a default bean Define your own @Bean Custom ObjectMapper
Disable configuration Use exclude or spring.autoconfigure.exclude DataSourceAutoConfiguration
Change the web server Exclude Tomcat and add Jetty spring-boot-starter-jetty

Starters

A Spring Boot Starter is a carefully selected group of dependencies for one application capability. Instead of finding and adding every library separately, you add one starter and Spring Boot brings in the commonly required libraries.

How a starter helps create an application feature
Choose a capability
spring-boot-starter-web
Spring MVC + JSON + embedded server
Auto-configuration creates the web infrastructure

Example: adding the Web starter

To build REST APIs, add the Web starter to pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

This starter commonly provides Spring MVC, an embedded servlet container, JSON serialization through Jackson, validation support, logging, and the Spring libraries needed to run a web application. You can then write a controller without manually configuring a DispatcherServlet or servlet container.

@RestController
@RequestMapping("/hello")
class HelloController {
    @GetMapping
    String hello() {
        return "Hello Spring Boot";
    }
}

Popular Spring Boot starters

Starter Use it for What it usually provides
spring-boot-starter Core applications Core Spring Boot support, logging, and configuration
spring-boot-starter-web REST APIs and MVC applications Spring MVC, Jackson, validation, and embedded Tomcat
spring-boot-starter-webflux Reactive web applications WebFlux and a reactive HTTP stack
spring-boot-starter-data-jpa Relational database access Spring Data JPA, Hibernate, transactions, and repositories
spring-boot-starter-jdbc SQL access with JDBC JDBC template and connection-pool support
spring-boot-starter-security Authentication and authorization Security filters and secure defaults
spring-boot-starter-validation Validating request objects Jakarta Bean Validation integration
spring-boot-starter-test Automated testing JUnit, Mockito, AssertJ, and Spring Test
spring-boot-starter-actuator Monitoring and operations Health, metrics, and management endpoints
spring-boot-starter-aop Aspect-oriented programming Spring AOP and AspectJ integration support
spring-boot-starter-cache Caching application results Spring's cache abstraction and related support

Starter dependencies and transitive dependencies

A starter may depend on several other libraries. These are called transitive dependencies. For example, the Web starter brings in the Spring Web modules and an embedded server without requiring you to list each one manually.

spring-boot-starter-web
├── spring-boot-starter
├── spring-web
├── spring-webmvc
├── spring-boot-starter-json
├── spring-boot-starter-tomcat
└── spring-boot-starter-validation

The exact dependency tree can vary with the Spring Boot version. Use Maven to inspect the actual libraries in your project:

mvn dependency:tree

Starters and auto-configuration are different

A starter provides libraries; auto-configuration uses those libraries to create default Beans and infrastructure.

Starter versus auto-configuration
StarterAdds compatible libraries
Classpath detectionBoot sees available classes
Auto-configurationCreates matching Beans

Adding spring-boot-starter-data-jpa gives the project JPA libraries. Auto-configuration then creates infrastructure such as an EntityManagerFactory and transaction support when the required database settings are available.

Dependency version management

When a Spring Boot parent or dependency management is used, Boot selects compatible versions for managed libraries. In most projects, do not add versions to individual Spring Boot starters.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>YOUR_SPRING_BOOT_VERSION</version>
</parent>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Specifying random versions for Spring libraries can create conflicts. Add a version only when you intentionally need a library version outside Boot's dependency management and have verified compatibility.

Starters with Gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Choosing the right starter

  • Use spring-boot-starter-web for traditional Servlet-based REST APIs.
  • Use spring-boot-starter-webflux when your application is designed for reactive, non-blocking processing.
  • Use spring-boot-starter-data-jpa for object-relational mapping and repository-based SQL access.
  • Use spring-boot-starter-jdbc when you prefer SQL and JDBC templates instead of JPA.
  • Use spring-boot-starter-test in the test scope so test libraries are not packaged as production dependencies.

Adding a non-Spring library

Not every library has a Spring Boot starter. You can add a normal Maven dependency and configure it yourself, or define a small @Bean for the third-party client.

<dependency>
    <groupId>com.example</groupId>
    <artifactId>third-party-client</artifactId>
    <version>1.0.0</version>
</dependency>

@Configuration
class ClientConfiguration {
    @Bean
    ThirdPartyClient thirdPartyClient() {
        return new ThirdPartyClient();
    }
}
Key idea: A starter is a convenient dependency bundle, not a magic replacement for application design. Select the capability you need, inspect its dependencies when troubleshooting, and customize or exclude parts only when there is a clear reason.

Dependency Injection

Dependency injection (DI) means a class receives the objects it needs instead of creating them with new. Spring owns these objects as beans and connects them when the application context starts.

@Service
class GreetingService {
    String message() { return "Hello from Spring"; }
}

@RestController
class GreetingController {
    private final GreetingService service;

    GreetingController(GreetingService service) {
        this.service = service;
    }
}
Constructor injection flow
Spring scansFinds @Service
Creates beanBuilds GreetingService
Injects itCalls controller constructor

Constructor injection is preferred because the dependency is visible, can be stored in a final field, and can be replaced with a test double in a unit test.

Why dependency injection is useful

Without DI, a class creates its own dependencies. This creates tight coupling and makes it difficult to replace a real database, payment provider, or email service during testing.

// Tight coupling: the service chooses its implementation
class OrderService {
    private final EmailSender sender = new SmtpEmailSender();
}

// Loose coupling: Spring supplies the implementation
@Service
class OrderService {
    private final EmailSender sender;

    OrderService(EmailSender sender) {
        this.sender = sender;
    }
}

The second design depends on the EmailSender abstraction. The implementation can be changed through configuration or a test double without changing the business class.

Types of dependency injection

Type Example When to use
Constructor injection Service(Repository repository) Required dependencies; recommended default
Setter injection setFormatter(Formatter formatter) Optional or replaceable dependency
Field injection @Autowired private Repository repository Generally avoid; hides dependencies and complicates tests
@Service
class ReportService {
    private Formatter formatter;

    @Autowired
    void setFormatter(Formatter formatter) {
        this.formatter = formatter;
    }
}

When a class has one constructor, Spring automatically uses it. You normally do not need @Autowired on that constructor. If a class has multiple constructors, mark the constructor Spring should use.

Dependency injection and testing

Constructor injection makes a class easy to test without starting Spring. A fake or mock dependency can be passed directly to the constructor.

InvoiceRepository fakeRepository = new InMemoryInvoiceRepository();
InvoiceService service = new InvoiceService(fakeRepository);

// The unit test can exercise InvoiceService without a database
service.createInvoice();

Application Context

The ApplicationContext is Spring's central container. It creates beans, stores them, injects dependencies, publishes events, and manages configuration. In a web application it also connects the application to the embedded server and request infrastructure.

The ApplicationContext as the application container
ConfigurationClasses and properties
ApplicationContextCreates and manages beans
ApplicationUses connected services

Component Scanning

Component scanning searches the application packages for classes marked with annotations such as @Component, @Service, @Repository, and @Controller. Spring registers each discovered class as a bean.

@SpringBootApplication
public class DemoApplication { }

@Service
class OrderService { }

@Repository
class OrderRepository { }

Keep the main application class in a parent package of these classes. If a component is outside the scan range, Spring will not discover it unless you configure scanning explicitly.

Autowiring

Autowiring is the process of resolving a dependency and supplying it to a bean. Constructor injection is the clearest form. Use @Qualifier when more than one bean has the same interface.

interface PaymentGateway {
    void pay();
}

@Component("cardGateway")
class CardGateway implements PaymentGateway { public void pay() { } }

@Service
class CheckoutService {
    private final PaymentGateway gateway;

    CheckoutService(@Qualifier("cardGateway") PaymentGateway gateway) {
        this.gateway = gateway;
    }
}
Best practice: Prefer one constructor and constructor injection. Avoid field injection because it hides required dependencies and makes plain unit tests harder to create.

What happens when multiple Beans match?

Spring normally resolves a dependency by type. If only one PaymentGateway Bean exists, it is injected automatically. If two or more implementations exist, Spring cannot guess which one you want and may throw NoUniqueBeanDefinitionException.

@Component("cardGateway")
class CardGateway implements PaymentGateway { }

@Component("paypalGateway")
class PaypalGateway implements PaymentGateway { }

// Error: two PaymentGateway Beans match this dependency
CheckoutService(PaymentGateway gateway) { }

Choosing a Bean with @Qualifier

Use @Qualifier when the injection point must select a specific implementation. The qualifier value normally matches the Bean name.

@Service
class CheckoutService {
    private final PaymentGateway gateway;

    CheckoutService(
            @Qualifier("paypalGateway")
            PaymentGateway gateway) {
        this.gateway = gateway;
    }
}

Choosing a default Bean with @Primary

Use @Primary when one implementation should be the default whenever no qualifier is supplied.

@Primary
@Component
class CardGateway implements PaymentGateway { }

@Component
class PaypalGateway implements PaymentGateway { }

// CardGateway is selected because it is @Primary
CheckoutService(PaymentGateway gateway) { }
Choice Meaning Best use
@Qualifier Choose one exact Bean at this injection point The required implementation is known here
@Primary Choose one default Bean among several One implementation is the normal application choice
Both @Qualifier wins over @Primary Use a default but allow explicit overrides

Injecting all implementations

Spring can inject a List, Set, or Map of matching Beans. This is useful when an application should use every notification channel or payment check.

@Service
class NotificationService {
    private final List<NotificationSender> senders;

    NotificationService(List<NotificationSender> senders) {
        this.senders = senders;
    }

    void notifyAllChannels(String message) {
        senders.forEach(sender -> sender.send(message));
    }
}

Use @Order on implementations when the execution order matters.

Optional dependencies

If a dependency is genuinely optional, use Optional or ObjectProvider instead of allowing a required constructor dependency to fail at startup.

@Service
class ReportService {
    private final Optional<AuditService> auditService;

    ReportService(Optional<AuditService> auditService) {
        this.auditService = auditService;
    }
}

Common DI and autowiring errors

  • NoSuchBeanDefinitionException: the required Bean is missing, outside component scanning, or missing an annotation.
  • NoUniqueBeanDefinitionException: multiple Beans match; use @Qualifier or @Primary.
  • Circular dependency: two Beans require each other; redesign the responsibilities or introduce a third service.
  • Null dependency: an object was created with new instead of being obtained from Spring, so Spring never injected its dependencies.
Best practice: Prefer constructor injection, depend on interfaces where practical, use @Qualifier for an explicit choice, use @Primary for a sensible default, and keep services stateless.

Bean Scopes

A bean scope controls how many instances Spring creates and how long they are kept.

Scope Meaning Good use
singleton One instance per application context. This is the default. Stateless services and repositories
prototype A new instance each time it is requested. Short-lived, stateful objects
request One instance for each HTTP request. Request-specific web state
session One instance for each HTTP session. User-session state
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
class TemporaryReport { }
Important: A singleton bean should usually be stateless. If it stores changing user data in fields, different requests may accidentally share that data.

Bean Life Cycle

The bean lifecycle is the sequence Spring follows from creating an object to removing it. Knowing the sequence helps you place setup and cleanup code correctly.

Bean lifecycle at a glance
1. InstantiateCreate the object
2. InjectProvide dependencies
3. InitializeRun setup callbacks
4. UseBean serves requests
5. DestroyRun cleanup
  1. Instantiation: Spring creates the object using its constructor.
  2. Dependency injection: Spring supplies constructor, field, or setter dependencies.
  3. Post-processors: framework hooks can inspect or enhance the bean.
  4. Initialization: methods marked with @PostConstruct run after dependencies are ready.
  5. Ready for use: the bean handles application work within its scope.
  6. Destruction: during context shutdown, @PreDestroy can release resources.
@Component
class ConnectionManager {
    @PostConstruct
    void connect() {
        System.out.println("Open connection");
    }

    @PreDestroy
    void disconnect() {
        System.out.println("Close connection");
    }
}

Use lifecycle callbacks for resources that belong to the bean, such as opening and closing a client. Avoid putting long-running work in initialization because it can delay application startup.

Detailed lifecycle order

For a singleton Bean, Spring applies callbacks in a predictable order. The exact internal path can vary when proxies or custom post-processors are involved, but the following sequence is the important learning model:

Detailed Spring Bean lifecycle
1. Read Bean definition
2. Constructor creates object
3. Aware callbacks
4. Inject properties and dependencies
5. Before-initialization processors
6. Initialization callbacks
7. After-initialization processors and ready Bean
  1. Read the definition: Spring reads metadata from annotations, configuration classes, XML, or a starter.
  2. Instantiate: Spring calls the constructor and creates the object.
  3. Aware callbacks: selected callback interfaces receive Spring objects such as the Bean name or context.
  4. Populate properties: constructor, setter, field, and other dependencies are supplied.
  5. Before initialization: registered BeanPostProcessor implementations can inspect the Bean.
  6. Initialize: @PostConstruct, InitializingBean, and a configured initMethod run.
  7. After initialization: post-processors can wrap the Bean with a proxy, for example for transactions or AOP.
  8. Use: the ready Bean is used by controllers, services, repositories, or other Beans.
  9. Shutdown: destruction callbacks release resources when the ApplicationContext closes.

Lifecycle callback options

Callback When it runs Recommendation
@PostConstruct After dependency injection Preferred for small initialization tasks
InitializingBean.afterPropertiesSet() After properties are set Useful when implementing Spring-specific lifecycle behavior
@PreDestroy Before the Bean is destroyed Preferred for cleanup tasks
DisposableBean.destroy() During context shutdown Use when Spring interface coupling is acceptable
initMethod/destroyMethod Configured on a @Bean method Useful for third-party classes that cannot be edited

Initialization interfaces

InitializingBean lets a Bean run code after Spring has supplied its properties.

@Component
class SearchIndex implements InitializingBean {
    private final IndexClient client;

    SearchIndex(IndexClient client) {
        this.client = client;
    }

    @Override
    public void afterPropertiesSet() {
        client.verifyConnection();
    }
}

For application code, @PostConstruct is often less coupled to Spring. Do not implement both for the same setup work, or it may run twice.

Destruction interfaces

DisposableBean provides a destroy() callback during a graceful ApplicationContext shutdown.

@Component
class FileResource implements DisposableBean {
    private final FileChannel channel;

    FileResource(FileChannel channel) {
        this.channel = channel;
    }

    @Override
    public void destroy() throws IOException {
        channel.close();
    }
}

Lifecycle methods for @Bean objects

Use initMethod and destroyMethod when the object belongs to a third-party library and cannot be annotated.

@Configuration
class ClientConfiguration {
    @Bean(initMethod = "connect", destroyMethod = "close")
    ExternalClient externalClient() {
        return new ExternalClient();
    }
}

class ExternalClient {
    public void connect() { /* open client */ }
    public void close() { /* release client */ }
}

BeanNameAware, BeanFactoryAware, and ApplicationContextAware

Aware interfaces let a Bean receive information about the Spring container. They are useful for framework infrastructure, but ordinary application services should prefer normal dependency injection.

@Component
class ContainerAwareBean implements
        BeanNameAware, BeanFactoryAware,
        ApplicationContextAware {

    @Override
    public void setBeanName(String name) {
        System.out.println("Bean name: " + name);
    }

    @Override
    public void setBeanFactory(BeanFactory factory) {
        System.out.println("BeanFactory received");
    }

    @Override
    public void setApplicationContext(
            ApplicationContext context) {
        System.out.println("ApplicationContext received");
    }
}

These callbacks are invoked before the initialization callbacks. Direct access to the container increases coupling, so inject the specific dependency you need whenever possible.

BeanPostProcessor

A BeanPostProcessor can run custom logic before and after every Bean initialization. Spring uses this extension point for annotations, proxies, AOP, and other framework features.

@Component
class TimingPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(
            Object bean, String name) {
        System.out.println("Before: " + name);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(
            Object bean, String name) {
        System.out.println("After: " + name);
        return bean;
    }
}

Always return the Bean, or a deliberate replacement/proxy, from these methods. Returning null can prevent the Bean from continuing through the processor chain.

BeanFactoryPostProcessor

BeanFactoryPostProcessor runs against Bean definitions before normal Bean objects are created. It changes metadata such as property values or scope; it does not perform initialization on a particular Bean.

@Component
class DefinitionLogger implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(
            ConfigurableListableBeanFactory factory) {
        System.out.println(
                "Definitions: " + factory.getBeanDefinitionCount());
    }
}

SmartInitializingSingleton

SmartInitializingSingleton runs after all regular singleton Beans have been instantiated. It is useful when a component must start work only after the whole singleton graph is ready.

@Component
class MessageConsumer implements SmartInitializingSingleton {
    @Override
    public void afterSingletonsInstantiated() {
        System.out.println("All singleton Beans are ready");
    }
}

Lifecycle and SmartLifecycle

These interfaces manage components that need to start and stop, such as message consumers, schedulers, or background listeners.

@Component
class EventListener implements SmartLifecycle {
    private boolean running;

    @Override
    public void start() {
        running = true;
        System.out.println("Listener started");
    }

    @Override
    public void stop() {
        running = false;
        System.out.println("Listener stopped");
    }

    @Override
    public boolean isRunning() {
        return running;
    }
}

SmartLifecycle also supports automatic startup, asynchronous shutdown, and phase ordering. Use it for application-managed processes rather than simple one-time setup.

Prototype and destruction behavior

Spring initializes prototype Beans, but it does not normally call their destruction callbacks because the container does not track every prototype instance after handing it to the application. The code that creates or owns a prototype resource must release it.

Important lifecycle practices

  • Keep @PostConstruct quick; slow network calls can delay application startup.
  • Close connections, executors, files, and clients in a destruction callback.
  • Do not rely on cleanup during a forced process kill; use graceful shutdown where possible.
  • Do not use ApplicationContextAware as a replacement for dependency injection.
  • Use BeanPostProcessor and BeanFactoryPostProcessor mainly for framework or infrastructure code.

Configuration Properties

Configuration properties keep environment-specific values outside Java code. They can come from application.properties, YAML files, environment variables, command-line arguments, or a profile-specific file.

How Spring Boot supplies configuration
Properties file, YAML, or environment variable
Spring Boot property sources
Bind or inject values
Bean uses typed application settings

Using @Value for individual values

@Value is convenient when a Bean needs one or two simple values. The placeholder uses the format ${property.name}.

# application.properties
app.name=Order Service
app.timeout-seconds=10

@Component
class AppInfo {
    @Value("${app.name}")
    private String name;

    @Value("${app.timeout-seconds:5}")
    private int timeoutSeconds;
}

The value after the colon is a default. If app.timeout-seconds is missing, Spring uses 5. A missing property without a default usually causes a startup error.

@Value can also read environment variables and simple expressions, but too many fields make a class difficult to read and test.

Using @ConfigurationProperties for related settings

Use @ConfigurationProperties when settings belong to one logical group. Spring binds properties with the same prefix to a typed object.

# application.properties
app.mail.host=smtp.example.com
app.mail.port=587
app.mail.username=notifications
app.mail.enabled=true

@ConfigurationProperties(prefix = "app.mail")
public record MailProperties(
        String host,
        int port,
        String username,
        boolean enabled) { }

Register the class with configuration-property scanning:

@SpringBootApplication
@ConfigurationPropertiesScan
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

It can then be injected like any other Spring Bean:

@Service
class MailService {
    private final MailProperties properties;

    MailService(MailProperties properties) {
        this.properties = properties;
    }

    void send(String message) {
        if (properties.enabled()) {
            System.out.println("Sending through " + properties.host());
        }
    }
}

YAML and nested configuration

YAML is useful when configuration contains nested objects or lists.

app:
  name: Order Service
  mail:
    host: smtp.example.com
    port: 587
    enabled: true
  features:
    - orders
    - reports

Both kebab-case and camel-case property names can bind to Java fields, but using kebab-case in configuration is a clear convention:

app.mail-timeout=10

@ConfigurationProperties(prefix = "app")
class AppProperties {
    private Duration mailTimeout;
    // getter and setter
}

Validating configuration

Validation catches incorrect configuration during startup instead of allowing a broken value to fail later during a request. Add the validation starter when it is not already available.

@Validated
@ConfigurationProperties(prefix = "app.mail")
public class MailProperties {
    @NotBlank
    private String host;

    @Min(1)
    @Max(65535)
    private int port;

    // getters and setters
}

If the port is outside the valid range or the host is empty, Spring Boot fails fast and reports the configuration problem.

Creating configuration Beans with @Bean

@Bean registers the return value of a method in the ApplicationContext. Use it for third-party classes, custom defaults, or objects that require explicit construction.

@Configuration
class AppConfiguration {
    @Bean
    Clock applicationClock() {
        return Clock.systemUTC();
    }
}

@Service
class AuditService {
    private final Clock clock;

    AuditService(Clock clock) {
        this.clock = clock;
    }
}

Spring calls the configuration method, stores the returned object as a Bean, and injects the same Bean wherever Clock is required. You can give it a name with @Bean("utcClock") and select it with @Qualifier("utcClock").

Configuring a RestTemplate Bean

RestTemplate is a synchronous HTTP client. It is useful when a Spring Boot service needs to call another REST service and wait for its response. Configure it as a Bean so the application can reuse one consistently configured client.

@Configuration
class HttpClientConfiguration {
    @Bean
    RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder
                .setConnectTimeout(Duration.ofSeconds(3))
                .setReadTimeout(Duration.ofSeconds(5))
                .build();
    }
}

RestTemplateBuilder is supplied by Spring Boot. The timeouts prevent a slow or unavailable remote service from holding application threads forever.

Calling a REST API with RestTemplate

@Service
class CustomerClient {
    private final RestTemplate restTemplate;

    CustomerClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    Customer findCustomer(Long id) {
        return restTemplate.getForObject(
                "https://api.example.com/customers/{id}",
                Customer.class,
                id);
    }
}

Common methods include:

Method Use
getForObject GET and convert the response body to an object
getForEntity GET and receive status, headers, and body
postForObject POST a request and convert the response
exchange Full control over HTTP method, headers, body, and response type
delete Send a DELETE request

Sending headers and reading a full response

HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<CustomerRequest> request =
        new HttpEntity<>(customerRequest, headers);

ResponseEntity<Customer> response = restTemplate.exchange(
        url,
        HttpMethod.POST,
        request,
        Customer.class);

Customer customer = response.getBody();

Handling errors from remote services

By default, RestTemplate throws exceptions for many 4xx and 5xx responses. Catch and translate those exceptions at the client boundary rather than exposing implementation details to controllers.

try {
    return restTemplate.getForObject(url, Customer.class);
} catch (HttpClientErrorException.NotFound ex) {
    throw new CustomerNotFoundException();
} catch (RestClientException ex) {
    throw new RemoteServiceException("Customer API unavailable", ex);
}

RestTemplate or WebClient?

RestTemplate is blocking and easy to understand. For new applications that need reactive, non-blocking processing, use Spring's WebClient. Choose based on the architecture rather than mixing blocking calls into a reactive pipeline.

Best practice: Use @Value for a few simple values, @ConfigurationProperties for related settings, @Bean for reusable configured objects, and a shared, timeout-configured HTTP client for external API calls. Never hard-code passwords, API tokens, or production URLs in Java source code.

Profiles

Profiles let one application use different configuration and Beans for different environments. The same code can run locally, in testing, and in production while using the appropriate database, ports, logging levels, and external services.

How profiles choose environment-specific settings
Start the application
Read spring.profiles.active
Load common properties
Load the selected profile properties
Create only the matching Beans

Profile-specific property files

Keep shared settings in application.properties and environment-specific values in files such as application-dev.properties and application-prod.properties.

# application.properties - shared by every environment
spring.application.name=order-service
spring.jpa.show-sql=false

# application-dev.properties - safe local settings
server.port=8081
spring.datasource.url=jdbc:h2:mem:orders
logging.level.com.example.orders=DEBUG

# application-prod.properties - production settings
server.port=8080
spring.datasource.url=jdbc:postgresql://db:5432/orders
logging.level.root=INFO

# Select a profile when starting the application
java -jar app.jar --spring.profiles.active=dev

In a deployment environment, the active profile can also be selected with an environment variable:

SPRING_PROFILES_ACTIVE=prod

Using @Profile with Beans

A profile can decide which implementation Spring creates. This is useful when development should use a fake service while production should use a real cloud service.

public interface EmailSender {
    void send(String to, String message);
}

@Profile("dev")
@Service
class ConsoleEmailSender implements EmailSender {
    public void send(String to, String message) {
        System.out.println("DEV email to " + to + ": " + message);
    }
}

@Profile("prod")
@Service
class SmtpEmailSender implements EmailSender {
    public void send(String to, String message) {
        // Send through the production mail provider
    }
}

With the dev profile active, Spring registers ConsoleEmailSender. With the prod profile active, it registers SmtpEmailSender.

Activating profiles

# application.properties
spring.profiles.active=dev

# Command line (usually useful in deployment scripts)
java -jar app.jar --spring.profiles.active=prod

# Maven test run
mvn test -Dspring.profiles.active=test

Command-line arguments and environment variables are commonly preferred for deployment because the same packaged JAR can be promoted through environments without changing the application code.

Real-world use cases

Use case Development Production
Database H2 or a local Docker database Managed PostgreSQL or MySQL
External APIs Mock server or sandbox endpoint Live payment, email, or shipping provider
Logging Detailed DEBUG logs Concise INFO logs
Security Test users and relaxed local settings Real authentication and strict security rules
Data loading Sample records for learning No sample data; use controlled migrations

Testing with a test profile

A test profile can provide an isolated database and predictable settings so tests do not modify development or production data.

# src/test/resources/application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.hibernate.ddl-auto=create-drop

@ActiveProfiles("test")
@SpringBootTest
class OrderServiceTest {
    // Tests use the test database and test configuration
}

Profile groups

When several profiles always belong together, define a profile group:

spring.profiles.group.production=prod,monitoring,security

# Activating production also activates the three related profiles
spring.profiles.active=production
Best practice: Keep common values in application.properties, put only differences in profile files, and keep passwords and tokens in environment variables or a secret manager. Profiles organize configuration; they are not a security system.

Conditional Beans

Spring Boot can create a bean only when a condition is true. This is the same idea used by auto-configuration and is useful for optional integrations.

@Bean
@ConditionalOnProperty(name = "app.payments.enabled", havingValue = "true")
PaymentGateway paymentGateway() {
    return new CardGateway();
}
Core idea: Spring Boot combines conditions, configuration, and component scanning to build the application context that your code uses.

When to Override Defaults

Keep Boot defaults when they match your needs. Define an explicit bean or property when you need a different port, database, serializer, security rule, or implementation. Prefer a small, deliberate override over disabling large parts of auto-configuration.

Debugging tip: Enable the condition evaluation report when you need to understand why an auto-configuration matched or did not match. Run with --debug flag.

Common Spring Boot Annotations Explained

Annotation Purpose Example usage
@SpringBootApplication Main entry point. Combines @Configuration, @EnableAutoConfiguration, and @ComponentScan. Mark your main class
@Component Generic Spring-managed component. Generic utility class
@Service Business logic layer. Semantically marks service classes. Implement business rules
@Repository Data access layer. Can translate database exceptions. Database access code
@RestController HTTP API controller. Combines @Controller and @ResponseBody. REST endpoints
@Bean Explicitly register a method's return value as a bean. Third-party object setup
@Autowired Inject dependencies. (Less common with modern constructor injection) Field/method injection
@Value Inject a property value into a field. Single configuration values
@Configuration Class contains @Bean definitions. Configuration classes
@ConfigurationProperties Bind properties to a typed object. Group related settings

Spring Boot Startup Flow

These concepts work together during application startup:

┌─ Application starts ─┐
│  main(String[] args)  │
└──────────┬────────────┘
           │
           ▼
┌──────────────────────────────────┐
│  SpringApplication.run()          │
│  Creates the Application Context │
└──────────┬───────────────────────┘
           │
           ▼
┌──────────────────────────────────┐
│  Auto-Configuration kicks in      │
│  • Checks classpath libraries     │
│  • Reads application.properties   │
│  • Conditions are evaluated       │
└──────────┬───────────────────────┘
           │
           ▼
┌──────────────────────────────────┐
│  Component Scanning               │
│  • Finds @Service classes         │
│  • Finds @Repository classes      │
│  • Finds @RestController classes  │
└──────────┬───────────────────────┘
           │
           ▼
┌──────────────────────────────────┐
│  Beans are created                │
│  • Instantiate marked classes     │
│  • Inject dependencies            │
│  • Call @PostConstruct methods    │
└──────────┬───────────────────────┘
           │
           ▼
┌──────────────────────────────────┐
│  Embedded Server starts           │
│  • Tomcat, Netty, or Jetty        │
│  • Listen on configured port      │
└──────────┬───────────────────────┘
           │
           ▼
┌──────────────────────────────────┐
│  🎯 Application Ready!            │
│  • Accept requests                │
│  • Serve responses                │
└──────────────────────────────────┘

Quick Visual: What Spring Boot Does

❌ Without Spring Boot

  • Install Tomcat separately
  • Create web.xml
  • Configure DispatcherServlet
  • Set up database connection pool
  • Configure Spring context XML
  • Manage dependency versions
  • Deploy WAR file
  • ~400+ lines of configuration

✅ With Spring Boot

  • Add starter dependency
  • Write your service code
  • Write your controller
  • Run main() method
  • Server starts automatically
  • Everything works!
  • Run as JAR: java -jar app.jar
  • ~50 lines of code + minimal config

Understanding the Request Flow

When a user's browser makes a request to your Spring Boot application, here's what happens:

Client makes HTTP request:
  GET /api/users/1

         ▼

Spring DispatcherServlet receives the request
  (automatically configured by Spring Boot)

         ▼

Request is matched to a @RestController method
  Example: getUser(@PathVariable Long id)

         ▼

Dependencies are injected into controller
  (UserService, UserRepository, etc.)

         ▼

Business logic executes in service layer
  (Fetch data from database, apply rules)

         ▼

Response is converted to JSON
  (Spring automatically serializes to JSON)

         ▼

HTTP response sent back to client
  200 OK + JSON body

Spring Boot Core Concepts Visual Guide

These concepts work together during application startup:

From dependencies to a ready application
Add starter dependencies
Start SpringApplication.run()
Create the Application Context
Run auto-configuration and component scanning
Create beans and inject dependencies
Apply profiles, properties, scopes, and conditions
Start the server and mark the application ready

Core Concepts Summary

Concept Simple meaning
Application Context The central container that manages Spring Beans.
Auto-Configuration Creates common configuration from dependencies and properties.
Starters Convenient dependency groups for application capabilities.
Component Scanning Finds annotated classes and registers them as Beans.
Dependency Injection Provides required objects instead of creating them manually.
Autowiring Resolves and supplies matching Beans.
Bean Scopes Control how many instances exist and how long they live.
Bean Lifecycle Controls creation, initialization, use, and destruction.
Configuration Properties Keep settings outside Java code.
Profiles Use different settings and Beans for different environments.
Conditional Beans Create Beans only when a condition matches.
First Project Combines these ideas in a working REST API.
Recommended next step: Follow the Spring Initializr project setup, then build the Product REST API and apply each core concept as you go.
Next: Setup and Installation →

Continue learning