Creating a Spring Boot Application
Generating the project for `BACKEND-101`, with only the dependencies the ticket needs.
আপনি একটি free preview lesson দেখছেন।
Until now, we have intentionally avoided writing production code. We worked from the product requirement through acceptance criteria, engineering context, the backlog, the technical design, the RFC and ADRs to the design review. The design is now clear enough, and it is time to begin implementation with our first ticket, BACKEND-101 Bootstrap Order Management Backend.
In this lesson, we will create a minimal but production-oriented Spring Boot application. The goal is not to create as many folders, dependencies or abstractions as possible. The goal is:
Create a clean, reproducible Java application foundation on which the entire Order Management Backend can evolve.
Ticket: BACKEND-101
Our initial backlog defined the ticket as:
# BACKEND-101 — Bootstrap Order Management Backend
## Context
We need the initial Spring Boot application for backend implementation.
## Scope
- Create the Gradle project.
- Configure Spring Boot.
- Establish the application entry point.
- Add baseline project configuration.
- Verify the application can build and start locally.
## Acceptance Criteria
- The project builds successfully with Gradle.
- The Spring Boot application starts successfully.
- The repository contains the agreed baseline project structure.
- A clean checkout can be built without IDE-specific setup.
## Out of Scope
- Product APIs
- Database schema
- Authentication
- Order logic
- Payment integration
- Production Docker setup
This scope will guide our implementation.
Start With the Ticket, Not Spring Initializr
Creating a Spring Boot project technically takes only a few minutes. But a backend engineer's first question should not be "Which checkboxes should I select?" The better question is:
What is the minimum required to complete this ticket correctly?
Right now, we need a Java project with a Gradle build, the Spring Boot runtime, an application entry point, basic configuration and a repeatable build. We do not currently need PostgreSQL, Spring Data JPA, Flyway, Spring Security, an HTTP client, Testcontainers or a payment SDK. Those will arrive when later tickets actually require them.
Avoid Dependency Shopping
A common mistake when creating a Spring Boot project is immediately adding Spring Web, Spring Data JPA, PostgreSQL, Spring Security, Redis, Kafka, Actuator, Validation, WebFlux and Lombok, because "we will probably need them eventually". That creates a larger dependency surface, more configuration, more startup behaviour, more things to understand and more accidental architecture.
Our principle is:
Add a dependency when a current engineering requirement needs it.
The fact that a dependency may become useful later is not enough reason to install it now.
Technology Choices and Repository Name
Our engineering context already fixed the language (Java), the framework (Spring Boot) and the build tool (Gradle), so we do not repeat a technology comparison here. The project repository is order-management. Initially, it contains only the Gradle build and src/; files such as docs/, compose.yaml and a Dockerfile arrive with the work that needs them.
Generating the Project With Spring Initializr
Spring Initializr (start.spring.io, also built into IntelliJ IDEA and VS Code) generates the starting project, so nobody writes the wrapper or the first build file by hand. The choices follow directly from the ticket:
| Field | Value | Why |
|---|---|---|
| Project | Gradle - Groovy | Our agreed build tool |
| Language | Java | Our agreed language |
| Spring Boot | 4.1.2 | The current release the team pins |
| Group | com.tarkev | Our organisation |
| Artifact and name | order-management | The repository and application name |
| Package name | com.tarkev.ordermanagement | Set explicitly; the generated default would contain an underscore |
| Packaging | Jar | Spring Boot runs as an executable JAR |
| Java | 21 | Our Java baseline |
| Dependencies | none | BACKEND-101 needs none yet |
The generated project contains the Gradle Wrapper, build.gradle, settings.gradle, the main class, one test, src/main/resources/application.properties, a .gitignore, a .gitattributes (line endings for the wrapper scripts) and a HELP.md. We keep almost all of it, review every file, and make two small changes described below: configuration in YAML and a README.md in place of HELP.md. A generator is a starting point, not a decision we stop owning.
The Gradle Wrapper and Reproducible Builds
A Gradle project should be built with ./gradlew build (gradlew.bat build on Windows) instead of requiring every developer to install and manually manage a matching Gradle version. The Gradle Wrapper consists of gradlew, gradlew.bat and the gradle/wrapper/ directory, and these files are committed with the project. The wrapper's properties file pins the Gradle distribution the project uses:
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip
On first use, the wrapper downloads exactly that version, so a developer machine, the CI environment and another engineer's machine all build with the same Gradle. This directly supports our acceptance criterion: a clean checkout can be built without IDE-specific setup.
A weak project setup "works from IntelliJ on my machine". A stronger one works with git clone followed by ./gradlew build in any supported Java environment. Developers may use IntelliJ IDEA, VS Code or another IDE, but the IDE is not the source of truth for the build: the repository and the Gradle build definition are. This becomes especially important when CI is introduced, because CI has no IDE at all.
The Build Files
settings.gradle has a simple responsibility: it defines the Gradle project name. We do not need application architecture configuration there.
rootProject.name = 'order-management'
For our choices, Spring Initializr generates this build.gradle:
plugins {
id 'java'
id 'org.springframework.boot' version '4.1.2'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.tarkev'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
The three plugins each have one job. java compiles and tests Java code. org.springframework.boot adds the Spring Boot tasks we use below, such as bootRun and bootJar. io.spring.dependency-management imports Spring Boot's managed dependency versions, which is why the starters have no version numbers: the Spring Boot version decides them, so they always fit together. The junit-platform-launcher line is there because Gradle 9 no longer puts the JUnit Platform launcher on the test runtime classpath by itself.
The versions are pinned on purpose, and 0.0.1-SNAPSHOT is simply an early development artifact version; release versioning belongs to later delivery work, not to BACKEND-101.
Why Java Toolchains?
The java { toolchain { ... } } section declares the Java baseline the project expects. Gradle compiles, tests and runs the application with a Java 21 JDK, whichever JDK happens to start Gradle itself, and fails with a clear message when it cannot find one instead of silently using another version. Without it, one engineer building with Java 21, another with a different version and CI with a third could see subtle differences in compilation, language features and runtime behaviour.
Why Only spring-boot-starter?
At this point, the ticket requires only a Spring Boot application that can start successfully. The core starter, spring-boot-starter, is sufficient: it brings Spring Boot, auto-configuration, logging and YAML support. We are not exposing REST endpoints yet. Later work brings its own starters:
- REST and API work:
spring-boot-starter-webmvc - Persistence work:
spring-boot-starter-data-jpa, the PostgreSQL driver andspring-boot-starter-flyway - Security work:
spring-boot-starter-security
Dependencies should arrive with a reason. The test starter, spring-boot-starter-test (JUnit Jupiter, AssertJ, Mockito and Spring's test support), is already here because testing is part of our Definition of Done: the Testing module deepens the strategy, but every change already carries tests appropriate to its behaviour.
Source Structure and Base Package
Our project follows the standard Java structure, and Spring Boot works naturally with it:
src/
├── main/
│ ├── java/ application source code
│ └── resources/ application resources and configuration
└── test/
└── java/ automated tests
Our Tarkev namespace is com.tarkev.ordermanagement, so application source lives under src/main/java/com/tarkev/ordermanagement/. Java package names provide a namespace, organise the code and, in Spring Boot, mark the component scanning boundary. Consistency matters more than the exact choice: once the convention is established, we do not need repeated discussions about ordermanagement versus orderservice versus order_management.
Why the Main Class Belongs at the Root
Our application entry point is com.tarkev.ordermanagement.OrderManagementApplication. This placement works naturally with Spring Boot's default component scanning:
com.tarkev.ordermanagement
├── OrderManagementApplication
├── product
├── inventory
└── order
Spring can discover application components in subpackages beneath the main class's package. That makes the root a natural home for the main application class.
Application Entry Point
Create the main class:
package com.tarkev.ordermanagement;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderManagementApplication {
public static void main(String[] args) {
SpringApplication.run(OrderManagementApplication.class, args);
}
}
A normal Java application begins with public static void main(String[] args), and Spring Boot does not change that Java rule: the JVM still starts from main. Inside it, SpringApplication.run(...) bootstraps Spring:
Diagram আঁকা হচ্ছে…
We will explore the Application Context in more detail later in this module.
@SpringBootApplication
The @SpringBootApplication annotation marks our main Spring Boot application configuration. For now, think of it as the application bootstrap annotation: it enables Spring Boot's core startup conventions, including auto-configuration and component discovery. Later lessons in this module look at the Beans and Application Context behind it; what matters now is the responsibility it fulfils, not the annotation name.
Application Configuration
Spring Initializr created src/main/resources/application.properties containing spring.application.name=order-management. Spring Boot supports several configuration formats, and for structured configuration this project uses YAML, so we replace that file with src/main/resources/application.yml:
spring:
application:
name: order-management
YAML is a project convention, not an architectural decision: what matters is consistency, not a debate about YAML versus .properties.
The configuration stays very small. It gives the application an explicit name, which later appears in logs, metrics, configuration and deployment; consistent naming from the beginning reduces drift. We do not add future database, payment or security configuration yet.
Even in the bootstrap phase, establish one habit: never commit secrets into configuration. This is bad when committed to Git:
payment:
api-key: super-secret-key
Credentials should come from external configuration or the environment. We will design production configuration more fully in the production-readiness module.
Initial Project Structure
After BACKEND-101, our project looks like this:
order-management/
├── build.gradle
├── settings.gradle
├── gradlew
├── gradlew.bat
├── gradle/
│ └── wrapper/
└── src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── tarkev/
│ │ └── ordermanagement/
│ │ └── OrderManagementApplication.java
│ └── resources/
│ └── application.yml
└── test/
└── java/
└── com/
└── tarkev/
└── ordermanagement/
└── OrderManagementApplicationTests.java
That is enough for the initial foundation. The product and order packages do not exist yet, because BACKEND-101 does not implement products or orders.
Avoid Architecture by Empty Folder
A poor bootstrap might create:
product/ api/ application/ domain/ persistence/ dto/ mapper/
inventory/ api/ application/ domain/ persistence/
order/ api/ application/ domain/ persistence/ dto/ mapper/
while every directory remains empty. That is not meaningful architecture; it is speculative folder generation. Good architecture becomes visible as real responsibilities arrive.
First Build
Run:
./gradlew build
Gradle reads build.gradle, resolves dependencies, compiles the main and test sources, runs the tests and packages the application. A successful build, an important part of BACKEND-101, ends with:
BUILD SUCCESSFUL
We build before running because starting the application directly from an IDE can hide repository-level build problems. ./gradlew build verifies the project definition, dependency resolution, compilation, tests and packaging, and we want the repository build to be healthy before feature work begins.
Running the Application
Run:
./gradlew bootRun
Spring Boot starts the application and logs its startup:
:: Spring Boot :: (v4.1.2)
INFO ... [order-management] ... : Starting OrderManagementApplication using Java 21.0.8 with PID 38668 (...)
INFO ... [order-management] ... : No active profile set, falling back to 1 default profile: "default"
INFO ... [order-management] ... : Started OrderManagementApplication in 0.336 seconds (process running for 0.478)
Then the process exits. At this stage, we have no HTTP server, because we deliberately have not added Spring Web, and nothing else keeps the application running. That is fine. Our acceptance criterion says "Spring Boot application starts successfully". It does not say "GET /api/v1/products works"; product APIs belong to later work. Notice also that the log already carries our application name, order-management.
./gradlew build also produces an executable JAR, build/libs/order-management-0.0.1-SNAPSHOT.jar, which runs without the IDE and without Gradle. This demonstrates that the application is genuinely runnable from its build artifact:
java -jar build/libs/order-management-0.0.1-SNAPSHOT.jar
First Test
Spring Initializr also generated a minimal Spring Boot bootstrap test:
package com.tarkev.ordermanagement;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class OrderManagementApplicationTests {
@Test
void contextLoads() {
}
}
The test appears almost empty, but its purpose is specific: Can Spring successfully create the application context? Suppose startup configuration later becomes invalid because of a missing required bean, broken dependency wiring or invalid Spring configuration. An application-context test catches these classes of bootstrap failure.
Don't overvalue a passing contextLoads(), though. It does not prove that business logic, database behaviour, the REST API or authorization are correct. For BACKEND-101, whose behaviours are "the project compiles, the context starts and the build succeeds", it is appropriate coverage; product, order and PostgreSQL tests arrive with those behaviours.
Build Output and .gitignore
After building, build/ contains generated output. That directory should not be committed to Git, because it is reproducible build output. The repository should contain source, the build definition, configuration, migrations and documentation rather than generated artifacts.
The generated .gitignore covers several IDEs; its essentials are:
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
.idea
*.iml
.vscode/
.DS_Store
Exact IDE rules depend on team conventions; .DS_Store is our own addition. The important principle is:
Do not commit machine-generated local state unless the repository intentionally owns it.
The Gradle Wrapper is different. gradlew, gradlew.bat and gradle/wrapper/ are intentionally versioned, because they are part of the project's reproducible build tooling. The !gradle/wrapper/gradle-wrapper.jar line exists for this reason: it keeps the wrapper JAR committed even when a global ignore rule excludes *.jar files.
README at Bootstrap Stage
Spring Initializr generates a HELP.md with links to Spring documentation. We replace it with a lightweight README.md that helps another engineer start the project:
# Order Management Backend
Backend application for the Commerce Order Management project.
## Requirements
- Java 21
## Build
```bash
./gradlew build
```
## Run
```bash
./gradlew bootRun
```
The README should describe what works now. Do not fill it with fictional future commands.
What We Deliberately Do Not Add
Each dependency we leave out is a decision, not an oversight. PostgreSQL belongs to BACKEND-102 Configure PostgreSQL and Flyway; adding it here would make first startup depend on database infrastructure and combine two separate concerns in one ticket. Spring Web (spring-boot-starter-webmvc) arrives with the first REST API, and Actuator when health checks and metrics become real production-readiness requirements. Lombok and MapStruct solve no current problem: modern Java gives us records and constructors, and we have no meaningful DTO-to-domain mapping yet.
Build Configuration Is Production Engineering
Developers sometimes think of src/main/java as the real code and build.gradle as setup. That distinction is misleading. The build defines the dependencies, the Java version, the plugins, test execution and packaging, and a broken build.gradle can block the entire team. Versions follow the same discipline: an upgrade can affect runtime behaviour, security posture and compatibility, so dependencies change as explicit engineering changes, not simply because a newer version exists.
Definition of Done Review
Now apply our project Definition of Done:
### Acceptance Criteria
- [x] `./gradlew build` succeeds
- [x] Spring Boot application starts
- [x] Baseline project structure exists
- [x] Build does not depend on IDE-specific setup
### Testing and Build
- [x] Spring application-context startup test added
- [x] Existing tests pass
- [x] Clean Gradle build succeeds
### Scope Discipline
- [x] No product API, database, security, payment or production Docker setup
The last section matters as much as the others. A feature is not more complete because it includes unrelated future work.
Example Pull Request
A reasonable first pull request could be:
## What
Bootstraps the Order Management Backend as a Spring Boot Gradle application.
## Why
Provides the application foundation required for subsequent backend capabilities.
## Changes
- Added Gradle project and wrapper
- Added Spring Boot application entry point
- Added baseline application configuration
- Added application-context bootstrap test
- Added basic project documentation
## Testing
- `./gradlew build`
- Application started with `./gradlew bootRun`
## Risks
Low. This introduces the initial application foundation and no business functionality.
This is a small, focused, reviewable PR.
What Should the Reviewer Check?
Review should focus on the ticket rather than cosmetic details. Useful questions include:
- Does a clean build work?
- Is the Java baseline explicit?
- Are unnecessary dependencies included?
- Is the package root correct?
- Can the application start outside the IDE?
- Is configuration minimal and safe?
- Does the change stay within
BACKEND-101scope?
This keeps review aligned with engineering intent. A good example of feedback:
PostgreSQL is included, but this ticket does not configure or use a database. Can we add it in BACKEND-102 instead?
The response is simply to remove the unused dependency; an empty package hierarchy for Product, Inventory and Order gets the same answer.
Small PR, Buildable Main Branch
Our first implementation PR is intentionally small: easy review, clear scope, simple rollback and a Git history that reads ticket by ticket (BACKEND-101, BACKEND-102 Configure PostgreSQL and Flyway, BACKEND-103 Model Product Domain). After BACKEND-101 merges, ./gradlew build should still succeed on the main branch, and this becomes an ongoing expectation: every future change begins from a healthy baseline and should leave one behind.
Practice Exercise 1: Generate and Compare
Solutions: lesson 3.09.
This exercise starts the order-management repository you will grow for the rest of the course. Create it in your workspace from module 1, next to docs/ (for example backend-course/order-management/).
- Generate the project with Spring Initializr using exactly the values in the table above, and unpack it. Before you change anything, list every file and compare the list with the lesson. Which file does the lesson's list of kept files not mention, and what is it for? What does the generated
.gitignorealready do withHELP.md? - Generate a second, throwaway project with the same values but leave the package name at its default. Which package does the main class end up in? Delete that project afterwards.
- In the real project, replace
application.propertieswith theapplication.ymlabove andHELP.mdwith the README, then run./gradlew build,./gradlew bootRunandjava -jar build/libs/order-management-0.0.1-SNAPSHOT.jar. Which log line shows the application name, and which shows that startup succeeded?
Practice Exercise 2: The First Commit
Run git init in the project and ./gradlew build once, so that build/ and .gradle/ exist. Before you look, write down which paths git status --short --untracked-files=all will list. Then run it, and check the three interesting paths with git check-ignore -v build .gradle gradle/wrapper/gradle-wrapper.jar, which prints the .gitignore rule that matches each path. Add .DS_Store to .gitignore and commit everything as BACKEND-101 Bootstrap Order Management Backend.
Practice Exercise 3: A Dependency Too Early
A teammate adds implementation 'org.springframework.boot:spring-boot-starter-data-jpa' to build.gradle, "because BACKEND-102 needs it anyway". As a throwaway experiment, add the line, predict what ./gradlew bootRun does now, run it, and remove the line again. Then write the review comment you would leave on the pull request.
Knowledge Check
Question 1
Every engineer already has Gradle installed. Why does the repository still commit the Gradle Wrapper?
Question 2
./gradlew bootRun logs Started OrderManagementApplication and the process exits. Why is that not a failure of BACKEND-101?
Question 3
The starters in build.gradle have no version numbers. What decides their versions, and what happens to them when the team raises the Spring Boot plugin version?
Question 4
Why can a project with a Java 21 toolchain still build correctly when Gradle itself is started by a different JDK?
Question 5
contextLoads() passes. Name one problem it would have caught and one it cannot catch.
Knowledge Check Answers
Answer 1
The wrapper pins one Gradle version for every machine and for CI, and downloads it on first use. Installed Gradle versions differ between machines, so "it builds for me" would depend on what each engineer happens to have.
Answer 2
The project has no web server or anything else that keeps the JVM alive, so once the context has started there is nothing left to do. The acceptance criterion is that the application starts, and the log proves it did.
Answer 3
The io.spring.dependency-management plugin imports the versions Spring Boot manages, so the plugin's Spring Boot version decides them. Raising the plugin version moves all starters together to versions tested with each other.
Answer 4
The toolchain tells Gradle to compile, test and run with a Java 21 JDK, independently of the JDK that launched Gradle. If no Java 21 is available, the build fails with a clear message instead of silently using another version.
Answer 5
It catches bootstrap failures, such as a missing required bean, broken dependency wiring or invalid Spring configuration. It cannot catch wrong business behaviour, wrong SQL or a wrong HTTP response, because it never exercises them.
Engineering Principle
A project bootstrap should establish the minimum reproducible foundation required for the next piece of meaningful work.
Two consequences follow. Dependencies, packages and infrastructure are added because the current system needs them, not because we expect every backend might eventually use them. And if the project only works through one developer's IDE, the project is not properly bootstrapped.
Summary
BACKEND-101is the first implementation ticket: a Gradle-based Spring Boot 4.1 project generated with Spring Initializr, with onlyspring-boot-starterandspring-boot-starter-test.- The repository build, not the IDE, is the source of truth: the committed Gradle Wrapper pins Gradle, and a Java toolchain pins Java 21.
- The package root is
com.tarkev.ordermanagement, withOrderManagementApplicationat the root so component scanning finds everything beneath it. @SpringBootApplicationmarks the bootstrap configuration, andSpringApplication.run(...)starts Spring from the ordinary Javamainmethod.- Configuration stays minimal (
spring.application.nameinapplication.yml), and secrets are never committed. ./gradlew build,./gradlew bootRunandjava -jarmust work from a clean checkout;build/is ignored while the wrapper files are committed.- A
contextLoads()test gives startup-level confidence for the bootstrap ticket, and testing begins with the work rather than being postponed. - PostgreSQL, Spring Web, Actuator, Lombok, MapStruct and empty capability packages wait until a requirement needs them; the result is a small, reviewable PR that leaves the main branch buildable.
In the next lesson, Understanding Spring Boot Project Structure, we examine the project we just created and how package placement affects Spring Boot's behaviour.
আগে ভাবুন
শীঘ্রই আসছেCheckpoint প্রশ্ন, যেখানে lesson উত্তর দেখানোর আগে আপনাকে একটি উত্তর বেছে নিতে হবে।