Typical Spring Layer Organization
A clean Spring application usually separates code into controller, service, repository, and model/entity layers.
com.example.app
├── AppApplication.java
├── controller
│ └── UserController.java
├── service
│ └── UserService.java
├── repository
│ └── UserRepository.java
├── entity
│ └── User.java
└── dto
├── CreateUserRequest.java
└── UserResponse.java
The usual request flow is:
HTTP Request
↓
Controller
↓
Service
↓
Repository
↓
Database
1. Controller Layer
The controller handles HTTP requests and responses.
Use:
@RestControllerfor JSON APIs@Controllerfor server-rendered pages such as Thymeleaf/JSP
Controllers should be thin. They should mainly:
- Accept requests
- Validate input
- Call services
- Return responses
Example:
package com.example.app.controller;
import com.example.app.dto.CreateUserRequest;
import com.example.app.dto.UserResponse;
import com.example.app.service.UserService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<UserResponse> findAll() {
return userService.findAll();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserResponse create(@Valid @RequestBody CreateUserRequest request) {
return userService.create(request);
}
}
2. Service Layer
The service contains business logic.
Use @Service.
Services should:
- Implement business rules
- Coordinate multiple repositories
- Handle transactions
- Convert between entities and DTOs if your app is small or medium-sized
Example:
package com.example.app.service;
import com.example.app.dto.CreateUserRequest;
import com.example.app.dto.UserResponse;
import com.example.app.entity.User;
import com.example.app.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional(readOnly = true)
public List<UserResponse> findAll() {
return userRepository.findAll()
.stream()
.map(user -> new UserResponse(
user.getId(),
user.getName(),
user.getEmail()
))
.toList();
}
@Transactional
public UserResponse create(CreateUserRequest request) {
User user = new User();
user.setName(request.name());
user.setEmail(request.email());
User savedUser = userRepository.save(user);
return new UserResponse(
savedUser.getId(),
savedUser.getName(),
savedUser.getEmail()
);
}
}
Use @Transactional on service methods rather than controller methods.
3. Repository Layer
The repository handles database access.
With Spring Data JPA, you usually define an interface that extends JpaRepository.
package com.example.app.repository;
import com.example.app.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
Spring Data JPA automatically provides common methods such as:
findAll()
findById(id)
save(entity)
deleteById(id)
You can also add query methods:
package com.example.app.repository;
import com.example.app.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
}
You generally do not need to annotate Spring Data repository interfaces with @Repository; Spring detects them automatically.
4. Entity Layer
The entity represents database tables.
Use Jakarta persistence imports:
package com.example.app.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
}
Entities should mostly represent persistent state. Avoid putting HTTP-specific logic in entities.
5. DTO Layer
DTOs separate your API contract from your database model.
Request DTO:
package com.example.app.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public record CreateUserRequest(
@NotBlank String name,
@Email @NotBlank String email
) {
}
Response DTO:
package com.example.app.dto;
public record UserResponse(
Long id,
String name,
String email
) {
}
Using DTOs helps avoid exposing internal entity fields directly through your API.
Recommended Responsibilities
| Layer | Annotation | Responsibility |
|---|---|---|
| Controller | @RestController, @Controller |
HTTP request/response handling |
| Service | @Service |
Business logic, transactions |
| Repository | Spring Data JpaRepository |
Database access |
| Entity | @Entity |
Database table mapping |
| DTO/Form | Records/classes with validation | API input/output models |
Dependency Direction
Keep dependencies flowing one way:
Controller → Service → Repository → Entity
Avoid this:
Repository → Service
Service → Controller
Entity → Controller
For example:
- A controller can inject a service.
- A service can inject a repository.
- A repository should not know about services or controllers.
- Entities should not depend on web/controller classes.
Best Practices
- Use constructor injection
@Service public class OrderService { private final OrderRepository orderRepository; public OrderService(OrderRepository orderRepository) { this.orderRepository = orderRepository; } } - Keep controllers thin
Bad:
@PostMapping public User create(@RequestBody User user) { if (user.getEmail() == null) { throw new IllegalArgumentException("Email is required"); } return userRepository.save(user); }Better:
@PostMapping public UserResponse create(@Valid @RequestBody CreateUserRequest request) { return userService.create(request); } - Put transactions in services
@Transactional public UserResponse create(CreateUserRequest request) { // business logic and repository calls } - Use DTOs for API boundaries
Do not expose entities directly unless the application is very small or internal.
-
Keep the main application class in the root package
com.example.app.AppApplication
That way Spring can scan:
com.example.app.controller
com.example.app.service
com.example.app.repository
com.example.app.entity
Feature-Based Alternative
For larger applications, you may prefer organizing by feature instead of technical layer:
com.example.app
├── user
│ ├── UserController.java
│ ├── UserService.java
│ ├── UserRepository.java
│ ├── User.java
│ ├── CreateUserRequest.java
│ └── UserResponse.java
├── order
│ ├── OrderController.java
│ ├── OrderService.java
│ ├── OrderRepository.java
│ └── Order.java
└── AppApplication.java
This is often easier to maintain as the project grows because related files stay together.
Simple Rule of Thumb
Ask this when deciding where code belongs:
- Is it about HTTP? Put it in the controller.
- Is it business logic? Put it in the service.
- Is it database access? Put it in the repository.
- Is it database structure? Put it in the entity.
- Is it request/response shape? Put it in a DTO.
For most Spring applications, the clean structure is:
Controller → Service → Repository → Database
with DTOs at the API boundary and entities at the persistence boundary.
