Appearance
Project Setup and Layered Architecture
Chapter 2 explained why Spring Boot exists. Now you build with it. This chapter does two things:
- Creates a project and runs it.
- Organises it in layers (controller, service, repository) and builds one API through all of them, so you see where each kind of code belongs and why.
Part 1: create and run a project
Step 1: generate it with Spring Initializr
Open start.spring.io and fill in:
| Field | Choose | Why |
|---|---|---|
| Project | Maven | the build tool (chapter 4). Gradle works too; this series uses Maven. |
| Language | Java | |
| Spring Boot | the latest stable version | |
| Group | com.example | usually your company's reversed domain |
| Artifact | learningspringboot | the project name |
| Packaging | Jar | see below |
| Java | 17 or 21 | Spring Boot 3 needs at least 17 |
| Dependencies | Spring Web | REST APIs plus an embedded Tomcat |
Group + artifact identify your project uniquely, and together they become the base package: com.example.learningspringboot.
Jar or War?
- JAR (Java archive): a standalone application. The server is embedded, so the application just starts and serves requests;
java -jar app.jaris all it needs. Microservices are built this way, and it is the normal choice today. - WAR (web archive): a bundle (Java classes plus HTML, CSS, JavaScript, JSP pages) made to be deployed into a separately installed servlet container. That was the way before Spring Boot.
Why only Spring Web? Start with what you need now. More dependencies (for example Spring Data JPA for databases) can be added to pom.xml at any time; later chapters add them when they need them.
Click Generate and unzip the download.
IntelliJ IDEA Ultimate can create the same project from File → New → Project → Spring Boot. The free Community edition cannot, so use the website and open the folder.
Step 2: open it and look around
The project contains:
text
learningspringboot/
├── pom.xml the build file: parent, dependencies
└── src/
├── main/
│ ├── java/com/example/learningspringboot/
│ │ └── LearningspringbootApplication.java the main class
│ └── resources/
│ ├── application.properties configuration (just spring.application.name for now)
│ ├── static/ static web files (unused for REST APIs)
│ └── templates/ server-side page templates (unused here)
└── test/java/... a generated testpom.xml has exactly two dependencies, both without versions (chapter 2 explained why):
spring-boot-starter-web: Spring MVC, JSON support, embedded Tomcat.spring-boot-starter-test: JUnit 5, Mockito and friends, for tests.
The main class:
java
package com.example.learningspringboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LearningspringbootApplication {
public static void main(String[] args) {
SpringApplication.run(LearningspringbootApplication.class, args);
}
}Step 3: run it
Run the main class from the IDE. The log ends with:
text
Tomcat started on port 8080 (http) with context path '/'
Started LearningspringbootApplication in 1.6 secondsThe server is up, but there is nothing to call yet: no controller exists. That comes next.
Part 2: the layered architecture
Almost every Spring Boot application, in small teams and large companies alike, splits its code into three layers:
| Layer | Annotation | Its one job |
|---|---|---|
| Controller | @Controller / @RestController | Host the API endpoints. The entry point for every request. |
| Service | @Service | All business logic. Not in controllers, not in repositories. |
| Repository | @Repository | Talk to the database (SQL or NoSQL): fetch, insert, update. |
Why bother? Each layer changes for a different reason. The API's shape changes in the controller only; business rules change in the service only; the database changes in the repository only. Nothing technically stops a service from querying the database itself, but then a database change ripples into business code, which is exactly what the layers prevent.
Next to the layers, a few supporting packages are common:
| Package | Holds | Why |
|---|---|---|
dto | Data Transfer Objects: plain classes carrying data in (request DTO) and out (response DTO) | keeps API field names separate from internal ones |
entity | classes that mirror database tables, one field per column (@Entity with JPA) | the repository's view of the data |
util | helper methods several classes need | no copy-paste between classes |
config + application.properties | configuration values | change behavior without changing code |
Why DTOs?
- Request DTO. Say the client sends
id. The controller copies it into aPaymentRequest, and the service and repository only ever seePaymentRequest. If the API later renamesidtosequenceNumber, only the controller's mapping line changes. - Response DTO. Say the table column is
payment_amount. The service maps the entity into aPaymentResponsewith a fieldamount. The client never sees your column names, and a column rename does not break the API.
Why configuration in application.properties?
Hard-coded values (int timeout = 10;) need a code change, a build and a deployment to change. Put the value in application.properties (payment.timeout=10) and read it in code (chapter 9 shows how with @Value). Then changing it is a configuration change, with no code change.
Part 3: one API through all the layers
Build GET /payments/{id}. The repository returns a fixed row here; connecting a real database is the JPA module.
The finished structure:
text
com/example/learningspringboot/
├── LearningspringbootApplication.java
├── controller/PaymentController.java
├── service/PaymentService.java
├── repository/PaymentRepository.java
├── dto/PaymentRequest.java
├── dto/PaymentResponse.java
└── entity/PaymentEntity.javaCreate the classes in the order the data needs them: the data classes first, then the layers from the bottom up.
Step 1: the request DTO
java
// File: PaymentRequest.java
public class PaymentRequest {
private Long paymentId;
public Long getPaymentId() { return paymentId; }
public void setPaymentId(Long paymentId) { this.paymentId = paymentId; }
}Step 2: the response DTO
What the client receives. These field names become the JSON keys.
java
// File: PaymentResponse.java
public class PaymentResponse {
private Long paymentId;
private double amount;
private String currency;
// getters and setters for all three
}Step 3: the entity
Mirrors the payment table, so its field names follow the columns, not the API.
java
// File: PaymentEntity.java
public class PaymentEntity {
private Long id;
private double paymentAmount;
private String paymentCurrency;
// getters and setters for all three
}Step 4: the repository
java
// File: PaymentRepository.java
@Repository
public class PaymentRepository {
public PaymentEntity getPaymentById(PaymentRequest request) {
// a real repository runs a query here; this one returns a fixed row
PaymentEntity entity = new PaymentEntity();
entity.setId(request.getPaymentId());
entity.setPaymentAmount(100.00);
entity.setPaymentCurrency("INR");
return entity;
}
}@Repository makes Spring create and manage this object (a bean, chapter 6), so the service can receive it.
Step 5: the service
java
// File: PaymentService.java
@Service
public class PaymentService {
@Autowired
PaymentRepository paymentRepository; // Spring supplies the repository bean
public PaymentResponse getPaymentDetailsById(PaymentRequest request) {
PaymentEntity entity = paymentRepository.getPaymentById(request);
// map the entity (database names) to the response DTO (API names)
PaymentResponse response = new PaymentResponse();
response.setPaymentId(entity.getId());
response.setAmount(entity.getPaymentAmount());
response.setCurrency(entity.getPaymentCurrency());
return response;
}
}Step 6: the controller
java
// File: PaymentController.java
@RestController
@RequestMapping("/payments")
public class PaymentController {
@Autowired
PaymentService paymentService;
@GetMapping("/{id}")
public PaymentResponse getPayment(@PathVariable Long id) {
// map the API input to the request DTO
PaymentRequest request = new PaymentRequest();
request.setPaymentId(id);
return paymentService.getPaymentDetailsById(request);
}
}(@RestController, @RequestMapping, @GetMapping and @PathVariable are chapter 5; @Autowired is chapter 7. For now, read them as "this class serves /payments", "GET /payments/{id} calls this method", and "Spring fills this field".)
Step 7: run and call it
Restart the application and open http://localhost:8080/payments/1:
json
{"paymentId":1,"amount":100.0,"currency":"INR"}The keys are the response DTO's names (amount, currency), not the entity's (paymentAmount, paymentCurrency): the service's mapping did that.
The request's path through the code:
Next: The project was built by Maven, which also downloaded every library behind those two starters. Chapter 4 explains Maven: the project structure it expects, pom.xml, and what happens when you run mvn package.
Check yourself
- Which two values together identify your project, and what else do they decide?
- When do you pick JAR, and when WAR?
- Which dependency gives you REST support and an embedded Tomcat, and why start with only that one?
- In which layer does business logic belong? Which layer should talk to the database? Why keep them apart?
- What problem does a request DTO solve? And a response DTO?
- What is an entity, and which layer mostly uses it?
- Why put a timeout value in
application.propertiesinstead of the code? - In the payment example, where does
paymentAmountbecomeamount?
Answers
- Group and artifact. They also form the base package name.
- JAR for a standalone application with an embedded server (the normal choice, used by microservices). WAR to deploy into a separately installed servlet container.
- Spring Web (
spring-boot-starter-web). Start with what you need; add dependencies topom.xmlwhen a feature needs them. - Service; repository. Each layer then changes for its own reason: a database change stays in the repository and does not ripple into business logic.
- Request DTO: the rest of the application never sees API field names, so renaming an API field changes only the controller's mapping. Response DTO: database column names never leak into the API.
- A class that mirrors a database table, one field per column. The repository layer.
- So it can change without a code change, build and redeployment.
- In the service, when it maps the
PaymentEntityto thePaymentResponse.