Spring Boot
Project Architecture
Spring Boot has a steeper learning curve compared to simpler frameworks like FastAPI due to its structured layout and vast ecosystem. Spring Boot allows you to build exteremely robust applications.
Spring Boot projects typically follow a layered architecture:
src/main/java/com/example/project/├── controller/ → REST API endpoints├── service/ → Business logic, validation, transformations├── repository/ → Database queries (JPA/Hibernate)├── model/ → Entity classes (map to database tables)├── dto/ → Data Transfer Objects (define request and response objects)├── security/ → Security features (authentication, middleware)├── exceptions/ → Custom exceptions (API error responses)├── config/ → Project or third-party library configurations├── util/ → Miscellaneous classes and methods└── Application.java → Main entry pointControllers
Controllers define your API endpoints and should be pretty simple. Logic such as data transformation and database queries should be handled in the service layer. The controller should make a call to the service and return it with minimal logic.
Each controller class defines a collection of endpoints. You define a controller class with the @RestController annotation at the top of the class. You can specify the base URL of the class with @RequestMaping("/api/example").
You define a method as an endpoint and how it will be handled with one of:
@GetMapping(GET request)@PostMapping(POST request)@PutMapping(PUT request)@DeleteMapping(DELETE request)
You can specify a deeper URL than the base URL for each endpoint, and define path parameters.
In each method, you can define parameters for the request body, path parameters, query parameters, security/authentication, and more.
Full example:
import org.springframework.web.bind.annotation.*;@RestController@RequestMapping("/api/users")public class UserController {private final UserService userService; // Spring Boot automatically injects an instance of UserService public UserController(UserService userService) { this.userService = userService; } @GetMapping("/{id}") // defines path parameter "id" public UserDto getUser(@PathVariable Long id) { return userService.getUserById(id); } @PostMapping("/create") // full path is /api/users/create public UserDto createUser(@RequestBody CreateUserDto dto) { return userService.createUser(dto); }}Dependency Injection
Spring Boot uses dependency injection (DI): it scans for classes annotated with @RestController, @Service, @Repository, @Component, etc., creates instances (called beans), and injects them wherever needed.
In the example above, the UserController class doesn't manually create a UserService instance. It simply defines the field and Spring Boot automatically injects it in.
So basically, you can define things such as services or repositories just as a variable, and use them like normal as if they were instantiated in the constructor.
Services
The service layer handles the meat of the work. Any business logic, data transformations, database queries (through repositories), or other error handling will be handled here. Methods you define here should be called in controllers and other services.
Add the @Service annotation to the top of your classes.
Full example:
import org.springframework.stereotype.Service;@Data@Servicepublic class UserService { private final UserRepository userRepository; // Spring injects UserRepository automatically public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public UserDto getUserById(Long id) { return userRepository.findById(id) .map(UserMapper::toDto) .orElseThrow(() -> new RuntimeException("User not found")); } public UserDto createUser(CreateUserDto dto) { User user = new User(dto.getName(), dto.getEmail()); return UserMapper.toDto(userRepository.save(user)); }}Repositories
Repositories let you interact with your database without writing boilerplate query code. This places a layer of abstraction over complex queries and makes calls readable in your services.
You must define an interface that extends one of Spring's built-in interfaces which provides some basic functionality. JpaRepository<T, ID> is the most common, which provides basic CRUD functionality.
You can define custom queries by method name (e.g. findByName, findByEmail) or manually using @Query if you prefer SQL. Spring requires a specific format for custom method names. If you cannot define your query this way, writing SQL with @Query is recommended.
Add the @Repository annotation to the top of your classes.
Full example:
import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.data.jpa.repository.Query;import org.springframework.stereotype.Repository;import com.exiger.supplierportal.model.UserAccount;import java.util.List;import java.util.Optional;@Repositorypublic interface UserAccountRepository extends JpaRepository<UserAccount, String> { // JpaRepository provides methods like save(), findById(), findAll(), deleteById(), etc // custom query using method name Optional<UserAccount> findByUserEmail(String userEmail); // custom query created manually with SQL string @Query("SELECT u.userEmail FROM UserAccount u") List<String> findAllUserEmails();}Models/Entities
Entities map Java objects to database tables using JPA annotations. There are a lot of different ones, but generally for basic data types having the @Entity annotation will be sufficient.
Common Annotations:
@Entity- Marks a class as a JPA entity (table)@Table- Defines the table name@Id- Primary key field@GeneratedValue- Define strategy for generating primary key when inserting@Column- Map a property (e.g. value constraints) to a specific column. Not required.@ManyToOneand@JoinColumn- Used in conjunction to define a foreign key column
Full example:
import jakarta.persistence.*;@Data@Entity@Table(name = "users")public class User { // primary key @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Column(unique = true) private String email; // foreign key column referencing "client_supplier_id" field of entity "ClientSupplier" @ManyToOne(optional = false) @JoinColumn(name = "client_supplier_id", nullable = false) private ClientSupplier clientSupplier;}DTOs
Data Transfer Objects (DTOs) are Java objects used to separate internal entity models from API request/response payloads.
These define your API payloads, which keeps API contracts stable even if the internal model changes. Spring Boot automatically parses JSON inputs into a DTO. If it fails to parse (e.g. types don't match up or missing fields), it will do error handling automatically (before you even get to your controller code).
It's best practice to create /request and /response folders within your /dto folder.
Full example:
public class UserAccountRequest { @NotBlank // marks field as required @Email(message = "Email is invalid") // Spring Boot checks that the input is an email private String userEmail; @NotBlank private String firstName; @NotBlank private String lastName;}Lombok
Lombok reduces boilerplate code like getters, setters, constructors.
PLEASE use this AS MUCH AS YOU CAN it is the most helpful library and will make your code so much easier to write and maintain.
Common annotations:
@Data– Generates getters, setters, toString, equals, hashCode@Getter,@Setter– Generate individually@NoArgsConstructor,@AllArgsConstructor– Generate constructors@Builder– Fluent builder pattern
Specifically use @Data and @AllArgsConstructor as well as @NoArgsConstructor to auto generate constructors and other utility functions.
Full example:
import lombok.Data;import jakarta.persistence.*;@Entity@Data // generates getters, setters, equals, hashCode, toStringpublic class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private double price;}Database Integration with PostgreSQL
Spring Boot integrates with PostgreSQL through Spring Data JPA and Hibernate.
Add configurations inside application.properties or application.yaml:
spring.datasource.url=jdbc:postgresql://localhost:5432/mydbspring.datasource.username=postgresspring.datasource.password=secretspring.jpa.hibernate.ddl-auto=updatespring.jpa.show-sql=truespring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialectHow it works:
- Hibernate inspects your
@Entityclasses - Creates tables/columns automatically if
ddl-auto=update - Queries run through repository interface methods (
findAll,save, etc.).
Summary
Controller → Defines REST endpoints (@RestController, @GetMapping, etc.). Uses dependency injection to get services.
Service → Business logic, injected with repositories and other services.
Repository → Database queries using Spring Data JPA.
Entity → Maps to database tables via Hibernate.
DTOs → Shape request/response objects for APIs.
Lombok → Reduces boilerplate with @Data, @Builder, etc.
PostgreSQL → Integrated via JPA/Hibernate with minimal config.