How to Create a Spring Boot Project (and What the Generator Actually Gives You)

Go to start.spring.io, pick your build tool and Java version, add the Spring Web dependency, and download the zip. On Spring Boot 4.1.1 that gives you ten files, a thirteen-line main class, and an embedded Tomcat on port 8080 that starts in under a second. The part worth reading is what those ten files are, and why the jar you build is 19 MB when one class in it is yours.

Creating a Spring Boot project takes about thirty seconds, and almost every guide to it stops there — download the zip, add a controller, done. That leaves you with a directory you did not write, containing files you have not read, building an artifact you cannot explain. This article does the thirty seconds first, and then spends the rest of its time on the interesting half: what the generator produced, and why the thing it builds is 19 MB when exactly one class in it is yours.

Everything below was generated, built and run rather than remembered, on Spring Boot 4.1.1.

The fastest path to a project that runs

Spring Initializr is the answer, and it is the answer the Spring team maintains rather than a community convention. Open it, and you are looking at a form: build tool, language, Spring Boot version, project metadata, and a dependency picker.

For a first project the only entry that matters is the dependency. Add Spring Web, leave everything else alone, and press Generate. You get a zip.

The same thing works without a browser, which is worth knowing because it is scriptable and because it shows you exactly what the form is choosing on your behalf:

curl https://start.spring.io/starter.zip \
  -d type=maven-project \
  -d javaVersion=21 \
  -d dependencies=web \
  -o demo.zip

Unzip it and you have a project. You need a JDK on the machine to build it — a JDK rather than a JRE, because something has to compile your code — and if you do not have one yet, installing Java 21 on Windows covers that part. The build tool comes with the project.

The choices that are expensive to change later

Most of the form is cosmetic. Four entries are not.

Choice Default Why it matters later
Group / Artifact com.example / demo Together they become your Java package, com.example.demo. Renaming later is a real refactor.
Build tool Gradle Decides whether you read pom.xml or build.gradle every day.
Java version 17 Your language floor. 21, 25 and 26 are also offered.
Packaging Jar Jar is right unless somebody hands you an application server.

Two of those defaults surprise people who last used Spring Boot a couple of years ago. The default project type is Gradle, not Maven. And the default Java version is 17 even though 21, 25 and 26 are on the list — Initializr picks the floor the framework supports, not the newest release available.

Set the group and artifact to something real before you generate. The package name is derived from them, it is written into every file, and it is the one thing on the form that is annoying to undo.

What actually landed on disk

Here is the entire generated Maven project, with the Spring Web dependency. Ten files:

.gitattributes
.gitignore
.mvn/wrapper/maven-wrapper.properties
HELP.md
mvnw
mvnw.cmd
pom.xml
src/main/java/com/example/demo/DemoApplication.java
src/main/resources/application.properties
src/test/java/com/example/demo/DemoApplicationTests.java

That is the whole thing. Worth naming individually:

  • mvnw and mvnw.cmd are the Maven Wrapper — a script that downloads the exact Maven version this project expects and runs the build with it. .mvn/wrapper/maven-wrapper.properties is where that version is pinned, and it pins Maven 3.9.16. This is why the instructions everywhere say ./mvnw rather than mvn: the wrapper means a colleague who has never installed Maven can still build your project, at your version rather than theirs.
  • pom.xml is the build file, and it is short — a parent, two dependencies, one plugin.
  • application.properties is not empty, which surprises people who expect a blank file. It carries one line: spring.application.name=demo. That name is what shows up in log output and in anything that reports on the running application.
  • DemoApplicationTests.java exists from minute one, and it asserts that the Spring context loads. It is a real test: if you later add a bean that cannot be constructed, this is what fails.
  • HELP.md is a generated "Getting Started" page of links into the reference documentation and the spring.io guides. No code, and safe to delete.

The nine lines that start everything

The generated main class, in full:

package com.example.demo;

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

@SpringBootApplication
public class DemoApplication {

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

}

An ordinary main method — the same entry point any Java program has. The framework is not a container you deploy into; it is a library your main starts.

The density is all in the annotation. @SpringBootApplication is one annotation that turns on three things:

  • @EnableAutoConfiguration — configure what is on the classpath. This is the piece that makes a web server appear later in this article without you writing a line about one.
  • @ComponentScan — find annotated classes, starting from the package this class is in.
  • @SpringBootConfiguration — let this class itself declare beans.

That second one has a consequence you will hit in week one. Component scanning starts at the package this class sits in and works downwards, which is why the generator puts DemoApplication in com.example.demo rather than in a com.example.demo.app subpackage. Your controllers, services and repositories go in packages underneath it. Put a class beside it in com.example.other and Spring will not find it, and nothing will tell you why — you will get a 404 for an endpoint that is definitely written.

Common mistake

Moving the main class into a subpackage "to tidy up" is the classic version of this. It breaks component scanning for everything above it, silently. If you want it elsewhere, you have to say so explicitly.

Run it, and read the log

./mvnw spring-boot:run

Here is what that actually printed, unchanged, on a freshly generated project:

 :: Spring Boot ::                (v4.1.1)

Starting DemoApplication using Java 21.0.12 with PID 214 (/app/target/classes started by root in /app)
No active profile set, falling back to 1 default profile: "default"
Tomcat initialized with port 8080 (http)
Starting Servlet engine: [Apache Tomcat/11.0.24]
Root WebApplicationContext: initialization completed in 428 ms
Tomcat started on port 8080 (http) with context path '/'
Started DemoApplication in 0.938 seconds (process running for 1.115)

Four things in there are worth reading properly.

A web server started, and you never asked for one. Apache Tomcat 11.0.24 is running on port 8080. You did not install it, configure it or start it — it arrived as a dependency because you ticked Spring Web, and auto-configuration started it because it was on the classpath. This is the single biggest difference from the older Java web model, where you built a war file and handed it to a server somebody else had installed.

"No active profile set" is not a warning. Profiles are named configuration sets — dev, prod — and you have none yet, so it says so and continues.

0.938 seconds. Worth knowing as a baseline, because it is the number that grows as you add dependencies, and noticing that it grew is how you catch something expensive being auto-configured.

Now open http://localhost:8080/:

{"timestamp":"...","status":404,"error":"Not Found","path":"/"}

A 404, and it is the correct result. Nothing is mapped to / because your project contains one class and that class contains no request mappings. The 404 is proof that a server accepted your connection and answered — a connection refused would be the failure. This is the point where every other tutorial hands you a @RestController, and this article stops, because that is a different question.

What "Spring Web" actually pulled in

Tick one box, and this is what appears in pom.xml:

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

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

Read that artifact name twice, because it is the detail most likely to trip you up when following an existing tutorial. On Spring Boot 4 it is spring-boot-starter-webmvc. The Spring Boot reference lists spring-boot-starter-web as deprecated in favour of it, and the same page records that the web starter uses Tomcat as its default embedded container.

That rename is a useful dating device. Almost everything written about creating a Spring Boot project names spring-boot-starter-web, because almost everything was written for Boot 3 or earlier. If a guide you are reading names the old starter, assume its other details need checking too.

Interview tip

A starter is not a library. It is a dependency whose only job is to bring in a coherent set of other dependencies that are known to work together at matching versions. spring-boot-starter-webmvc pulls Spring MVC, an embedded Tomcat and Jackson for JSON, and the versions are decided for you by spring-boot-starter-parent. That is the actual product being sold here: version coordination.

On Gradle the same two dependencies look like this, alongside two plugins — org.springframework.boot at 4.1.1 and io.spring.dependency-management at 1.1.7 — and a Java toolchain set to 21:

implementation 'org.springframework.boot:spring-boot-starter-webmvc'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'

Inside the 19 MB jar

Build the thing:

./mvnw package

The result is 19,904,307 bytes — just under 19 MB — and this is where the abstraction becomes worth understanding. That jar contains:

  • 34 dependency jars, under BOOT-INF/lib/
  • one class of yours, at BOOT-INF/classes/com/example/demo/DemoApplication.class

Nineteen megabytes for one class. What you actually bought is the other 34 files, and it is worth seeing what they are:

spring-core-7.0.9.jar          spring-webmvc-7.0.9.jar
spring-context-7.0.9.jar       tomcat-embed-core-11.0.24.jar
spring-beans-7.0.9.jar         jackson-databind-3.1.5.jar
spring-boot-4.1.1.jar          logback-classic-1.5.38.jar

Two things stand out. The Spring Framework jars are at 7.0.9 — Spring Boot 4.1.1 is a distribution built on Spring Framework 7, and the two version numbers are not the same thing. And Tomcat is right there as an ordinary dependency, tomcat-embed-core-11.0.24.jar, which is what "embedded server" literally means: a jar on your classpath rather than software on your machine.

The application is small; the platform it carries is not.

The manifest holds the trick that makes it work:

Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.example.demo.DemoApplication

java -jar does not run your class. It runs Spring Boot's JarLauncher, which is what the Spring Boot Maven plugin put there when it repackaged the build output. The launcher sets up a class loader that can read jars nested inside a jar — something the standard Java launcher cannot do — and only then calls the Start-Class, which is yours.

That indirection is the whole reason a Spring Boot application ships as one file you can copy anywhere a JVM exists. It is also why a Eureka service registry or any other Spring Boot server is deployed by moving a single jar rather than by installing anything.

Two traps, one of them only if you script it

The build fails and the project looks fine. The wrapper runs on whatever JVM it finds, and it finds it through JAVA_HOME. A project that will not build on a machine where java -version works is usually an environment problem rather than a project problem — setting JAVA_HOME correctly is the first thing to check, before you read a line of the build output.

The scripted-generation trap, which is genuinely obscure and cost me a build to find. Initializr's metadata endpoint publishes version identifiers in a legacy format that carries a .RELEASE suffix. Pass one of those to the starter.zip API verbatim:

curl https://start.spring.io/starter.zip -d bootVersion=4.1.1.RELEASE ...

...and the generator writes that string straight into your pom.xml as the parent version. There is no 4.1.1.RELEASE in Maven Central — the released versions are plain, like 4.1.1 — so the build dies with UnresolvableModelException, an error that names the parent POM and gives you no hint that a five-character suffix is the cause.

The fix is to omit bootVersion entirely and take the default, or to pass 4.1.1. The browser form never has this problem, which is exactly why it is confusing when a script does.

Tip

Generate a project and read all ten files before you write anything of your own. It takes five minutes once, and it is the difference between using a framework and being surprised by one.

Frequently asked questions

Do I need Maven or Gradle installed before I can build the project?
No. The generated project carries a wrapper — mvnw and mvnw.cmd for Maven, gradlew for Gradle — and the wrapper downloads the exact build tool version the project was generated with. The Maven wrapper pins 3.9.16. You do need a JDK; the wrapper is a build tool, not a Java runtime.
Why is my starter called spring-boot-starter-webmvc and not spring-boot-starter-web?
Because you are on Spring Boot 4. The reference documentation lists spring-boot-starter-web as deprecated in favour of spring-boot-starter-webmvc, and Initializr generates the new name. If a tutorial you are following names the old one, it was written for Boot 3 and other details in it are probably stale too.
Why does the application return 404 when I open localhost:8080?
Because nothing is mapped to that path yet. A freshly generated project has one class, and it contains no request mappings, so Spring Boot 4.1.1 answers with a JSON body of the shape {"timestamp":"...","status":404,"error":"Not Found","path":"/"}. That 404 is the proof the server is running — a connection-refused error would be the failure.
Should I pick Maven or Gradle?
Initializr defaults to Gradle, and either is a defensible choice. Pick the one the rest of your team already runs, because the build file is the thing you will read most often and the one you will ask colleagues about. If nothing decides it for you, Maven's pom.xml is more verbose and more predictable to read; Gradle's build.gradle is shorter and is a program.
Can I generate a project without opening a browser?
Yes. start.spring.io serves a starter.zip endpoint that takes the same choices as query parameters, so one curl command produces the same zip. Watch the version string if you script it — see the trap at the end of this article.

References

  1. Spring InitializrSpring
  2. Spring Boot Reference: Build SystemsSpring
  3. Using the @SpringBootApplication AnnotationSpring
  4. Spring Boot Reference: Structuring Your CodeSpring
  5. Spring Boot Maven PluginSpring
  6. Developing Your First Spring Boot ApplicationSpring
  7. Maven WrapperApache Maven
  8. spring-boot-starter-parent maven-metadata.xmlMaven Central