Skip to content
Chapter 03 · Spring Boot

Project Setup and Layered Architecture ​

Chapter 2 explained why Spring Boot exists. Now you build with it. This chapter does two things:

  1. Creates a project and runs it.
  2. 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:

FieldChooseWhy
ProjectMaventhe build tool (chapter 4). Gradle works too; this series uses Maven.
LanguageJava
Spring Bootthe latest stable version
Groupcom.exampleusually your company's reversed domain
Artifactlearningspringbootthe project name
PackagingJarsee below
Java17 or 21Spring Boot 3 needs at least 17
DependenciesSpring WebREST 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.jar is 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 test

pom.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 seconds

The 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:

LayerAnnotationIts one job
Controller@Controller / @RestControllerHost the API endpoints. The entry point for every request.
Service@ServiceAll business logic. Not in controllers, not in repositories.
Repository@RepositoryTalk 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:

PackageHoldsWhy
dtoData Transfer Objects: plain classes carrying data in (request DTO) and out (response DTO)keeps API field names separate from internal ones
entityclasses that mirror database tables, one field per column (@Entity with JPA)the repository's view of the data
utilhelper methods several classes needno copy-paste between classes
config + application.propertiesconfiguration valueschange behavior without changing code

Why DTOs? ​

  • Request DTO. Say the client sends id. The controller copies it into a PaymentRequest, and the service and repository only ever see PaymentRequest. If the API later renames id to sequenceNumber, only the controller's mapping line changes.
  • Response DTO. Say the table column is payment_amount. The service maps the entity into a PaymentResponse with a field amount. 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.java

Create 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 ​

  1. Which two values together identify your project, and what else do they decide?
  2. When do you pick JAR, and when WAR?
  3. Which dependency gives you REST support and an embedded Tomcat, and why start with only that one?
  4. In which layer does business logic belong? Which layer should talk to the database? Why keep them apart?
  5. What problem does a request DTO solve? And a response DTO?
  6. What is an entity, and which layer mostly uses it?
  7. Why put a timeout value in application.properties instead of the code?
  8. In the payment example, where does paymentAmount become amount?
Answers
  1. Group and artifact. They also form the base package name.
  2. JAR for a standalone application with an embedded server (the normal choice, used by microservices). WAR to deploy into a separately installed servlet container.
  3. Spring Web (spring-boot-starter-web). Start with what you need; add dependencies to pom.xml when a feature needs them.
  4. Service; repository. Each layer then changes for its own reason: a database change stays in the repository and does not ripple into business logic.
  5. 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.
  6. A class that mirrors a database table, one field per column. The repository layer.
  7. So it can change without a code change, build and redeployment.
  8. In the service, when it maps the PaymentEntity to the PaymentResponse.