PrepZone Logo
PrepZone

Controller → Service → Repository

The three layers every Boot REST app uses and what belongs in each one.

Why this matters

  • Layered architecture is the default pattern for Spring Boot REST services — violating it creates untestable controllers and duplicated business logic.
  • Clear boundaries let you swap JPA for JDBC or add caching without touching controllers.
  • Code reviews and interviews expect you to articulate what belongs in each layer.

The three layers

ControllerHTTP in/out — @RestController
ServiceBusiness rules — @Service
RepositoryDatabase — @Repository
DatabasePostgreSQL / H2
Each layer has one job. Dependencies point inward — controllers never touch the database directly.

Responsibilities in BookStore

  • Controller — Parse HTTP, validate input, call service, map to response DTO, set status codes. No business rules.
  • Service — Business logic, transaction boundaries, orchestration across repositories. No HTTP awareness.
  • Repository — Data access only. Queries, saves, deletes. No business validation.

Controller layer

Java
@RestController
@RequestMapping("/api/books")
public class BookController {
    private final BookService bookService;

    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    @GetMapping("/{id}")
    public BookResponse getBook(@PathVariable Long id) {
        return BookResponse.from(bookService.findById(id));
    }
}

The controller knows about HTTP (@GetMapping, status codes) and DTOs. It delegates all decisions to BookService.

Service layer

Java
@Service
@Transactional(readOnly = true)
public class BookService {
    private final BookRepository repository;

    public BookService(BookRepository repository) {
        this.repository = repository;
    }

    public Book findById(Long id) {
        return repository.findById(id)
            .orElseThrow(() -> new BookNotFoundException(id));
    }

    @Transactional
    public Book createBook(CreateBookRequest request) {
        if (repository.existsByIsbn(request.isbn())) {
            throw new DuplicateIsbnException(request.isbn());
        }
        Book book = new Book(request.title(), request.isbn(), request.price());
        return repository.save(book);
    }
}

Business rules live here: duplicate ISBN check, price validation, transaction demarcation.

Repository layer

Java
public interface BookRepository extends JpaRepository<Book, Long> {
    boolean existsByIsbn(String isbn);
    List<Book> findByGenre(String genre);
}

Pure data access. Spring Data generates the implementation from method names.

Dependency direction

Java
Controller → Service → Repository → Database

Never:

Forbidden dependencies

  • Controller → Repository (skips business logic)
  • Repository → Service (upward dependency)
  • Service → Controller (circular dependency)

DTOs at layer boundaries

Entities should not leak to the HTTP layer:

Java
public record BookResponse(Long id, String title, String isbn, BigDecimal price) {
    public static BookResponse from(Book book) {
        return new BookResponse(book.getId(), book.getTitle(),
            book.getIsbn(), book.getPrice());
    }
}

Controllers return BookResponse; services work with Book entities internally.

Package structure

Java
com.example.bookstore/
├── controller/
│   ├── BookController.java
│   └── AuthorController.java
├── service/
│   ├── BookService.java
│   └── AuthorService.java
├── repository/
│   ├── BookRepository.java
│   └── AuthorRepository.java
├── model/
│   ├── Book.java
│   └── Author.java
└── dto/
    ├── BookResponse.java
    └── CreateBookRequest.java

One package per layer keeps navigation predictable as BookStore grows.

Cross-cutting concerns

Logging, security, and caching sit outside the three layers via AOP and filters:

Cross-cutting placement

  • Security filter — Authenticates before the controller runs.
  • @Transactional — Applied on service methods, not controllers.
  • @ControllerAdvice — Centralises exception-to-HTTP mapping.

When to add more layers

AdditionTrigger
mapper packageComplex entity-to-DTO mapping (MapStruct)
client packageOutbound HTTP calls to other services
event packageDomain events between modules
  • mapper package

    TriggerComplex entity-to-DTO mapping (MapStruct)
  • client package

    TriggerOutbound HTTP calls to other services
  • event package

    TriggerDomain events between modules

Start with three layers. Split only when a layer grows unwieldy.

Quick recall

Everything you need if you only revisit this box.

  • Three layers: controller (HTTP), service (business logic), repository (data access).
  • Dependencies flow downward — never controller-to-repository or repository-to-service.
  • Services own @Transactional boundaries and business validation.
  • DTOs separate the HTTP contract from JPA entities.
  • Cross-cutting concerns (security, logging) use AOP and filters, not layer violations.
  • Organise packages by layer for predictable navigation in growing codebases.

Test yourself

Answer these before moving on — recall is what makes it stick.