In this tutorial, we will build a Spring Boot CRUD (Create, Read, Update, Delete) application using PostgreSQL as the database. The application will manage a Product entity with fields: id
, name
, description
, and price
. We will use Java record
for the DTO (Data Transfer Object) and follow best practices such as keeping the conversion logic in the service layer, using constructor-based dependency injection, and more.
Spring Boot Three-Layer Architecture
+-----------+ +-----------------+
| | | Controller Layer|
| Postman +----->+ (Handles HTTP |
| (Client) | | requests and |
+-----------+ | responses) |
+--------+--------+
|
v
+--------+--------+
| Service Layer |
| (Implements |
| business logic |
| and data |
| transformations)|
+--------+--------+
|
v
+--------+--------+
| Repository Layer|
| (Manages data |
| operations with|
| PostgreSQL) |
+--------+--------+
|
v
+--------+--------+
| PostgreSQL |
| (Database for |
| storing data) |
+-----------------+
Description of the diagram
- Postman (Client): Acts as the client interface for sending requests to and receiving responses from the Spring Boot application. Useful for testing and interacting with the API.
- Controller Layer: The entry point for all client requests, routing them to appropriate service handlers and returning responses. It’s crucial for decoupling the client from the core processing logic.
- Service Layer: Central to application operation, this layer contains essential business logic and data manipulation routines. It acts as a mediator between the web interface and data resources.
- Repository Layer: Specifically responsible for data persistence operations, interfacing directly with PostgreSQL to fetch, store, and update data as needed.
- PostgreSQL: The underlying relational database system used for storing all application data. It ensures data integrity and provides robust query capabilities.
What You’ll Learn:
- Setting up a Spring Boot project with PostgreSQL.
- Configuring Spring Boot to connect to PostgreSQL.
- Implementing CRUD operations with Spring Data JPA.
- Using Java
record
for DTOs to transfer data. - Testing REST APIs using Postman.
Prerequisites
Before starting, make sure you have:
- Java Development Kit (JDK) 17 or later
- Apache Maven (for project management)
- PostgreSQL (installed locally or via Docker)
- IDE (e.g., IntelliJ IDEA, Eclipse, or VS Code)
- Postman (to test the APIs)
Step 1: Setting Up the Project
1.1 Create a Spring Boot Project
- Open Spring Initializr.
- Configure the project metadata:
- Project: Maven
- Language: Java
- Spring Boot Version: Latest (3.x)
- Group:
com.example
- Artifact:
spring-boot-postgresql-crud
- Java Version: 17 or later
- Add the following dependencies:
- Spring Web: For building RESTful web services.
- Spring Data JPA: For interacting with PostgreSQL using JPA (Java Persistence API).
- PostgreSQL Driver: To connect Spring Boot with PostgreSQL.
- Spring Boot DevTools: For hot reloading during development.
- Click Generate to download the project, extract the zip file, and open it in your IDE.
Explanation:
Spring Initializr helps generate a pre-configured Spring Boot project with all the necessary dependencies.
PostgreSQL will be used as the external database, so we added the PostgreSQL driver to connect our Spring Boot app to it.
Step 2: Configuring PostgreSQL
2.1 Create a PostgreSQL Database
If you have PostgreSQL installed, create a new database for your application:
CREATE DATABASE productdb;
2.2 Configure application.properties
In the src/main/resources/application.properties
file, add the following configuration to connect Spring Boot to PostgreSQL:
# PostgreSQL database configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/productdb
spring.datasource.username=postgres
spring.datasource.password=yourpassword
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA configurations
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
Replace yourpassword
with your PostgreSQL password.
Explanation:
- spring.datasource.url: JDBC URL to connect to PostgreSQL. The format is
jdbc:postgresql://[host]:[port]/[database]
. - spring.datasource.username and password: Your PostgreSQL credentials.
- spring.jpa.hibernate.ddl-auto=update: Automatically updates the database schema based on the entity mappings.
Step 3: Creating the Product Entity
3.1 Create the Product Entity
In the model
package, create a Java class named Product
to represent the product entity in the database.
package com.example.springbootpostgresqlcrud.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private double price;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
}
Explanation:
- @Entity: Marks this class as a JPA entity, which will be mapped to a table in the PostgreSQL database.
- @Id: Defines the primary key field (
id
), which will be auto-generated using @GeneratedValue with theIDENTITY
strategy. - Fields: The product has
name
,description
, andprice
.
Step 4: Creating the Repository
4.1 Create ProductRepository
In the repository
package, create an interface ProductRepository
that extends JpaRepository
:
package com.example.springbootpostgresqlcrud.repository;
import com.example.springbootpostgresqlcrud.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}
Explanation:
- JpaRepository: Provides all the necessary CRUD methods like
save()
,findAll()
,findById()
, anddeleteById()
. - @Repository: Marks this interface as a Spring Data repository, enabling automatic CRUD operations.
Step 5: Using Java record for DTO
5.1 Create the ProductDTO Class
In the model
package, create a Java record
called ProductDTO
:
package com.example.springbootpostgresqlcrud.model;
public record ProductDTO(Long id, String name, String description, double price) {}
Explanation:
- Java
record
: A Java feature that automatically generates constructors, getters,toString()
,equals()
, andhashCode()
, making it ideal for use as a DTO to transfer data between the server and client.
Step 6: Creating the Service Layer
6.1 Create ProductService Interface
In the service
package, define an interface ProductService
:
package com.example.springbootpostgresqlcrud.service;
import com.example.springbootpostgresqlcrud.model.ProductDTO;
import java.util.List;
import java.util.Optional;
public interface ProductService {
List<ProductDTO> getAllProducts();
Optional<ProductDTO> getProductById(Long id);
ProductDTO saveProduct(ProductDTO productDTO);
ProductDTO updateProduct(Long id, ProductDTO productDTO);
void deleteProduct(Long id);
}
6.2 Implement ProductService in ProductServiceImpl
In the service implementation, add the conversion logic between Product (entity) and ProductDTO (DTO):
package com.example.springbootpostgresqlcrud.service;
import com.example.springbootpostgresqlcrud.model.Product;
import com.example.springbootpostgresqlcrud.model.ProductDTO;
import com.example.springbootpostgresqlcrud.repository.ProductRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class ProductServiceImpl implements ProductService {
private final ProductRepository productRepository;
public ProductServiceImpl(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public List<ProductDTO> getAllProducts() {
return productRepository.findAll().stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
@Override
public Optional<ProductDTO> getProductById(Long id) {
return productRepository.findById(id).map(this::convertToDTO);
}
@Override
public ProductDTO saveProduct(ProductDTO productDTO) {
Product product = convertToEntity(productDTO);
Product savedProduct = productRepository.save(product);
return convertToDTO(savedProduct);
}
@Override
public ProductDTO updateProduct(Long id, ProductDTO productDTO) {
Product product = productRepository.findById(id).orElseThrow();
product.setName(productDTO.name());
product.setDescription(productDTO.description());
product.setPrice(productDTO.price());
Product updatedProduct = productRepository.save(product);
return convertToDTO(updatedProduct);
}
@Override
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
// Convert Product Entity to ProductDTO
private ProductDTO convertToDTO(Product product) {
return new ProductDTO(product.getId(), product.getName(), product.getDescription(), product.getPrice());
}
// Convert ProductDTO to Product Entity
private Product convertToEntity(ProductDTO productDTO) {
Product product = new Product();
product.setName(productDTO.name());
product.setDescription(productDTO.description());
product.setPrice(productDTO.price());
return product;
}
}
Explanation:
- Conversion logic between DTO and Entity is placed in the service layer, ensuring the controller remains clean and focused on handling HTTP requests.
- convertToDTO: Converts a Product entity into a ProductDTO.
- convertToEntity: Converts a ProductDTO into a Product entity.
Step 7: Creating the REST Controller
In the controller
package, create the ProductController
to expose REST APIs:
package com.example.springbootpostgresqlcrud.controller;
import com.example.springbootpostgresqlcrud.model.ProductDTO;
import com.example.springbootpostgresqlcrud.service.ProductService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public List<ProductDTO> getAllProducts() {
return productService.getAllProducts();
}
@GetMapping("/{id}")
public ResponseEntity<ProductDTO> getProductById(@PathVariable Long id) {
Optional<ProductDTO> product = productService.getProductById(id);
return product.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
}
@PostMapping
public ProductDTO createProduct(@RequestBody ProductDTO productDTO) {
return productService.saveProduct(productDTO);
}
@PutMapping("/{id}")
public ResponseEntity<ProductDTO> updateProduct(@PathVariable Long id, @RequestBody ProductDTO productDTO) {
try {
ProductDTO updatedProduct = productService.updateProduct(id, productDTO);
return ResponseEntity.ok(updatedProduct);
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
}
Explanation:
- The controller provides endpoints for CRUD operations on products.
- The controller only handles HTTP requests and delegates business logic to the service layer.
Step 8: Running and Testing the Application
8.1 Running the Application
To run the application, open SpringBootPostgresqlCrudApplication.java
and click the Run button in your IDE, or run the following command in the terminal:
./mvnw spring-boot:run
8.2 Testing with Postman
You can test the REST APIs using Postman:
- GET all products:
- URL:
http://localhost:8080/api/products
- Response: A list of product DTOs.
- URL:
- GET product by ID:
- URL:
http://localhost:8080/api/products/{id}
- Response: The product data for the given ID.
- URL:
- POST create a new product:
- URL:
http://localhost:8080/api/products
- Body:
{ "name": "Laptop", "description": "A high-performance laptop.", "price": 1200.00 }
- URL:
- PUT update an existing product:
- URL:
http://localhost:8080/api/products/{id}
- Body:
{ "name": "Updated Laptop", "description": "An updated high-performance laptop.", "price": 1300.00 }
- URL:
- DELETE a product:
- URL:
http://localhost:8080/api/products/{id}
- Response: No content (HTTP 204).
- URL:
Conclusion
In this tutorial, we built a Spring Boot CRUD REST API with a PostgreSQL database, and we used Java record
as the DTO for data transfer. This guide offers a simple but effective way to build a CRUD application with Spring Boot and PostgreSQL, ensuring that the code is clean and follows best practices.