C
Spring Boot/Layered Architecture/Lesson 07

Layered Architecture — *Dividing Responsibilities into Layers*

30 min·theory

Layered Architecture — *Dividing Responsibilities into Layers*

🎯 After reading this lesson

By the end of this lesson, you will be able to confidently do the following 3 things.

  • ✅ Separate responsibilities across the Controller → Service → Repository 3-tier layers
  • ✅ Know the exact location where @Transactional should be applied
  • ✅ Understand who owns the responsibility of converting between DTOs and Entities

Keep the learning objectives as a checklist — close the lesson once you can answer all of them.

Why split into *3 layers*

The Core in One Line

A Spring backend typically divides code into 3 layers: Controller → Service → Repository. This is the standard approach for giving each layer a clear responsibility and producing maintainable code.

The Role of Each Layer

code
┌─────────────────────────────────────┐
│         @RestController              │  ← Receive HTTP, validate, convert DTOs
│  - @GetMapping·@PostMapping          │
│  - Validate request parameters       │
│  - Convert DTO → Service input       │
└──────────────┬──────────────────────┘
               │ Call Service
               ▼
┌─────────────────────────────────────┐
│           @Service                   │  ← Business logic
│  - Transaction (@Transactional)      │
│  - Coordinate multiple Repositories  │
│  - External API calls (email, payment)│
└──────────────┬──────────────────────┘
               │ Call Repository
               ▼
┌─────────────────────────────────────┐
│         @Repository                  │  ← DB access
│  - JpaRepository·QueryDSL            │
│  - Simple CRUD or queries            │
│  - Knows nothing beyond the DB       │
└─────────────────────────────────────┘

Controller: The HTTP gateway. Receives requests, validates, builds responses. No business logic.

Service: The heart of business logic. The unit of transaction. Coordinates multiple Repositories and external APIs.

Repository: Talks to the DB. Queries only. No business decisions.

Why split this way

1. Separation of concerns: Each layer does one thing well. If the Controller also runs DB queries, things become a mess.

2. Testability: When testing the Service, you can run unit tests without a real Controller or Repository — replace them with mocks.

3. Change isolation: If HTTP changes to GraphQL, you only modify the Controller. Service and Repository stay the same.

4. Transaction boundary: Attaching @Transactional to Service methods is the standard. Putting it on Controller or Repository creates ambiguous boundaries.

Real-world example

java
// === Controller ===
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @PostMapping
    public UserDto create(@RequestBody @Valid CreateUserDto dto) {
        return userService.create(dto);
    }
}

// === Service ===
@Service
@Transactional
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepo;
    private final EmailService emailService;

    public UserDto create(CreateUserDto dto) {
        // Business rule
        if (userRepo.existsByEmail(dto.email())) {
            throw new DuplicateEmailException();
        }
        // Save
        User saved = userRepo.save(User.from(dto));
        // Side effect
        emailService.sendWelcome(saved.getEmail());
        // DTO conversion
        return UserDto.from(saved);
    }
}

// === Repository ===
public interface UserRepository extends JpaRepository<User, Long> {
    boolean existsByEmail(String email);
}

The Controller is thin, the Service is thick with business rules, and the Repository handles only DB access — a clean structure.

DTO — Object that carries data between layers

Layers do not pass the same object directly. They convert through DTOs (Data Transfer Objects).

  • CreateUserDto — request body
  • User — JPA Entity (DB mapping)
  • UserDto — response object

Why keep them separate?

  • The Entity's internal structure is not exposed externally (security and encapsulation)
  • Sensitive fields like passwords are naturally excluded from responses
  • API changes have no impact on the Entity (loose coupling)

Java 14+ record is fantastic for creating DTOs:

java
public record CreateUserDto(@NotBlank @Email String email, @NotBlank String name) {}
public record UserDto(Long id, String email, String name) {
    public static UserDto from(User u) {
        return new UserDto(u.getId(), u.getEmail(), u.getName());
    }
}

Common Anti-patterns

1. Controller handles business logic: Breaks transactions and prevents reuse
2. Service accepts HttpServletRequest: Creates HTTP dependency, making testing and reuse difficult
3. Business rules in Repository: The same query ends up written elsewhere too
4. Returning the Entity directly: Exposes passwords and couples internal structure to the outside

Summary

The 3-layer architecture is the Spring standard. Responsibilities are divided so each layer does one thing well. Using DTOs to carry data between layers reduces coupling.

🤖 Try asking AI like this

Knowing the concepts from this lesson lets you give AI specific instructions. Instead of a vague 'fix this', you make requests with vocabulary — that's the starting point for saving tokens.

  • "Apply the Layered Architecture — Dividing Responsibilities into Layers pattern to this Spring Boot code"
  • "Write a @SpringBootTest integration test related to Layered Architecture — Dividing Responsibilities into Layers"
  • "Tell me 3 pitfalls to watch out for when using Layered Architecture — Dividing Responsibilities into Layers in production"

Why does this save tokens

Without knowing the concept, even after getting an AI response you have to ask 'What does that mean?' again. That follow-up question is what eats up tokens. Learn the concept once and the conversation ends in one go.

Layered Architecture — Responsibility by Layer - Spring Boot