← Series overview

Spring Boot from first principles · Module 02 of 05

Build the smallest useful Spring Boot application.

This module creates a real Boot project without a web server. Keeping the first application non-web makes it easier to see what the framework starts and what it does not.

After this module, you should be able to:
  • distinguish the Maven parent from a starter dependency
  • explain the three responsibilities grouped by @SpringBootApplication
  • predict the default component scan boundary
  • explain why spring-boot-starter does not start an embedded web server

Project shape

Three files are enough for the first run.

Directory layout
orders/
├── pom.xml
└── src/main/
    ├── java/com/example/orders/
    │   └── OrdersApplication.java
    └── resources/
        └── application.properties

The application class sits in the root package com.example.orders. Later components should live in that package or one of its subpackages so the default scan can find them.

Maven's standard layout separates Java source from runtime resources. Package names belong to Java; folders mirror those packages. Properties are resources placed on the classpath, so they live below src/main/resources rather than inside a Java package.

Java source

Compiled into bytecode. Classes, interfaces, records and annotations live below src/main/java.

Resources

Copied into the application classpath. Configuration, templates and static files can live below src/main/resources.

Maven responsibilities

The parent supplies build defaults; the starter supplies libraries.

spring-boot-starter-parent

A Maven parent that provides compatible dependency versions, plugin management and sensible compiler/resource defaults. It is not a jar added to the running application.

spring-boot-starter

A dependency bundle for core Boot support, auto-configuration and logging. It does not include the servlet stack required for an HTTP server.

spring-boot-maven-plugin

Builds an executable Boot jar and provides Maven goals such as spring-boot:run.

These three entries solve different build problems. The parent controls Maven defaults, the starter contributes dependencies to the application classpath, and the plugin changes how the application is run and packaged. Removing one does not mean the other two take over its job.

Why dependency management matters
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <!-- No version here -->
</dependency>

The starter version is omitted because the Boot parent manages a tested set of versions. If every Spring library were versioned independently, Maven could resolve a combination that compiles poorly or fails at runtime.

The application annotation

@SpringBootApplication combines three roles.

@Configuration

The class may contribute bean definitions and application configuration.

@EnableAutoConfiguration

Boot evaluates available classes, existing beans and configuration to decide which infrastructure it can provide.

@ComponentScan

Spring searches the application class package and its subpackages for component classes.

Conceptually, the composed annotation gives the application class the same three roles as the explicit form below:

Conceptual expanded form
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class OrdersApplication {
    // ...
}

Use @SpringBootApplication for the normal entry point. The expanded form is useful for understanding, not a recommendation to replace the composed annotation in every project.

Application entry point
package com.example.orders;

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

@SpringBootApplication
public class OrdersApplication {

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

SpringApplication.run(...) creates and refreshes an ApplicationContext. The method returns the context, but normal business code receives beans through injection instead of retrieving them manually.

Startup, step by step

SpringApplication.run builds and refreshes the container.

  1. 01

    Inspect the classpath

    Boot determines whether this looks like a servlet web application, reactive web application or non-web application.

  2. 02

    Prepare the environment

    Command-line arguments, system values and application properties become available as configuration.

  3. 03

    Create the appropriate ApplicationContext

    This project gets a non-web context because no servlet or reactive web application stack is present.

  4. 04

    Load bean definitions

    The application configuration, component scan results and matching auto-configurations contribute recipes for objects.

  5. 05

    Refresh the context

    Spring creates non-lazy singleton beans, resolves constructor dependencies and runs lifecycle callbacks.

  6. 06

    Run startup callbacks

    Boot invokes CommandLineRunner and ApplicationRunner beans after successful startup.

This is why a missing dependency usually prevents startup: the context must build a complete valid graph before the application begins normal work.

The scan boundary

The application class location is an architectural decision.

What the default scan includes
com.example.orders
├── OrdersApplication              // scan starts here
├── notification
│   └── EmailNotificationSender    // included
├── service
│   └── OrderService               // included
└── runner
    └── DemoRunner                 // included

com.example.payments
└── PaymentService                 // not included

@ComponentScan begins with the package containing OrdersApplication. It walks downward through subpackages; it does not walk upward to com.example and then down into sibling packages.

Good default

Put the application class at the root of one application's packages, then group features below it.

Common surprise

A correctly annotated service outside the root is still invisible to the default scan.

Predict startup behaviour

No servlet classes means no embedded web server.

Auto-configuration reacts to what is available. With only spring-boot-starter, Boot creates a non-web application context, logs that the application started and then exits when no non-daemon work remains. Adding web libraries later changes the conditions that match.

DependenciesApplication typeTypical result
spring-boot-starterNon-webCreates the context; no HTTP port is opened.
spring-boot-starter-webmvcServlet webConfigures Spring MVC and normally starts an embedded servlet server.
Reactive web starterReactive webCreates reactive web infrastructure when its conditions match.

@SpringBootApplication is present in all three cases. The annotation does not mean “start Tomcat.” It enables conditional configuration; the classpath supplies evidence about which kind of application is being built.

The first property

application.properties changes configuration, not Java structure.

application.properties
spring.application.name=orders

The application name can appear in logging, metrics and other infrastructure. It does not rename the Java class, Maven artifact or package. Those are separate identities that happen to use a consistent name in this example.

Four different names with different owners
Maven artifact:     orders
Java class:        OrdersApplication
Java package:      com.example.orders
Spring property:   spring.application.name=orders

Practice checkpoint

Write it before reading the solution.

Suggested time: 10 minutes

From an empty directory, create a minimal Maven Spring Boot application named orders.

  • Use Spring Boot 4.1.1 and Java 17.
  • Do not add a web dependency.
  • Use the package com.example.orders.
  • Set the application name in properties.
Hints, in increasing order
  1. Use Java 17 and Spring Boot 4.1.1 as this series' fixed baseline.
  2. The only runtime starter needed is spring-boot-starter.
  3. Keep the application class in com.example.orders and give its file the exact public class name.
Working solution
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

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

    <groupId>com.example</groupId>
    <artifactId>orders</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
OrdersApplication.java
package com.example.orders;

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

@SpringBootApplication
public class OrdersApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrdersApplication.class, args);
    }
}
application.properties
spring.application.name=orders

Run with mvn spring-boot:run. A successful startup does not include Tomcat, Jetty or Netty because no web stack is present.

Record after the attempt

Elapsed time · hint level (H0 means no hint; H1–Hn is the last hint used) · conceptual errors · mechanical errors · explanation confidence from 0–2.

Foundation check

Where will Spring scan?

With the application class in com.example.orders, the default scan covers that package and its subpackages. A component in com.example.payments is outside that boundary.