Skip to content

Expert · Spring Boot basics · topic 54 of 60 · 25 min

Layers: controller, service and repository

Explanation

Spring Boot backends are usually split into layers: controllers handle HTTP (parsing input, choosing status codes), services hold the business rules, and repositories talk to the database. DTOs carry data across the API boundary, entities map to tables. Each layer can be tested on its own: @WebMvcTest for controllers, plain unit tests for services, @DataJpaTest for repositories.

Why layers?

If one controller method parses JSON, checks business rules, writes SQL and builds the response, it becomes impossible to test or change safely. Layers give each class one kind of job. Rules like "a seat cannot be booked twice" live in exactly one place, the service, whichever way the request arrives (REST, a message queue or a scheduled job).

output
HTTP request
   │
   ▼
Controller   ─ parse input, validate format, map to/from DTOs, pick status codes
   │
   ▼
Service      ─ business rules, transactions, calls to other services
   │
   ▼
Repository   ─ load and save entities (SQL / JPA)
   │
   ▼
Database

A booking flow through the layers

java
// Controller: HTTP only
@PostMapping("/api/bookings")
public ResponseEntity<BookingDto> book(@Valid @RequestBody BookingRequest req) {
    Booking b = bookingService.book(req.trainNo(), req.date(), req.passengerName());
    return ResponseEntity.status(HttpStatus.CREATED).body(BookingDto.from(b));
}

// Service: the rules
@Service
public class BookingService {
    private final SeatRepository seats;
    private final BookingRepository bookings;
    public BookingService(SeatRepository seats, BookingRepository bookings) { this.seats = seats; this.bookings = bookings; }

    @Transactional
    public Booking book(String trainNo, LocalDate date, String passenger) {
        if (date.isBefore(LocalDate.now())) throw new IllegalArgumentException("Cannot book a past date");
        Seat seat = seats.findFirstFree(trainNo, date)
                .orElseThrow(() -> new NoSeatsAvailableException(trainNo, date));
        seat.markBooked();
        return bookings.save(new Booking(seat, passenger));
    }
}

// Repository: data access (Spring Data generates the implementation; next topic)
public interface BookingRepository extends JpaRepository<Booking, Long> { }

DTOs versus entities

An entity is a class mapped to a database table (next topic). A DTO (data transfer object), often a record, is the shape of data sent to or from clients. Keeping them separate means you can rename a column without breaking the mobile app, hide internal fields, and accept only the fields a client is allowed to send.

java
public record BookingDto(long id, String trainNo, String passenger, String seat) {
    public static BookingDto from(Booking b) {
        return new BookingDto(b.getId(), b.getSeat().getTrainNo(), b.getPassenger(), b.getSeat().getLabel());
    }
}

Testing each layer

LayerHow to testSpeed
ServicePlain JUnit + Mockito mocks for repositoriesMilliseconds
Controller@WebMvcTest with the service mocked; check status codes and JSONFast (web layer only)
Repository@DataJpaTest against an in-memory or Testcontainers databaseSeconds
Whole app@SpringBootTest, sometimes with a real database in TestcontainersSlowest; fewer of these
Careful: Two common layering mistakes: putting business rules in controllers (they get duplicated when a second entry point appears), and letting services return ResponseEntity or know about HTTP. Services should throw meaningful exceptions; the controller layer (or @RestControllerAdvice) decides the status code.
Think of it like this: Layers are like a restaurant: the waiter (controller) takes orders and serves plates, the chef (service) decides how dishes are cooked and follows the recipes, and the storekeeper (repository) fetches ingredients from the store room (database). The waiter never cooks, and the chef never walks to the tables.
Tip: A good test of your layering: could you call the service from a command-line tool or a scheduled job without any HTTP classes? If yes, the boundaries are clean.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Layers: controller, service and repository · 1/6

Layers

In this lesson you will organise a backend into layers.

Prefer a video by a teacher? Find free videos on “spring boot layered architecture controller service repository dto” on YouTube ↗

In real life: Adding WhatsApp bookings without duplicating rules

A bus-ticket company first took bookings only through its REST API. Later it added bookings through a WhatsApp chatbot that sends messages to a queue. Because all rules (no past dates, no double booking, fare calculation) were in BookingService, the new message listener simply called the same service method.

java
@Component
public class WhatsappBookingListener {
    private final BookingService bookingService;
    public WhatsappBookingListener(BookingService s) { this.bookingService = s; }
    public void onMessage(BookingMessage m) {
        bookingService.book(m.trainNo(), m.date(), m.passengerName());   // same rules as the REST API
    }
}

Practice (0/3 done)

Task 1 · Question

Where should the rule "a passenger cannot book a past date" live?

Task 2 · Question

Which is a layering mistake?

Task 3 · Practice

Draw (on paper or in a text file) the layers for an "apply leave" feature in an HR system: the controller endpoint and its request/response DTOs, the service method with at least three business rules (enough leave balance, no overlap with existing leave, not in the past), and the repository methods needed. Then write the service method in Java with the repositories mocked in a JUnit test.

Quick quiz

Question 1

1.What is a DTO?

Question 2

2.Which test annotation loads only the web layer for controller tests?

Question 3

3.Why keep business rules out of controllers?

Question 4

4.What does @Transactional on a service method give you?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What is a DTO?

Answer: An object that carries data across a boundary such as the API

Data transfer object.

Which test annotation loads only the web layer for controller tests?

Answer: @WebMvcTest

@WebMvcTest is a test slice for MVC controllers.

Why keep business rules out of controllers?

Answer: Other entry points (queues, jobs) would have to duplicate them

One place for rules.

What does @Transactional on a service method give you?

Answer: All database changes in the method commit or roll back together

Declarative transactions.

Which layer is optional in Google's recommended architecture?Related

Answer: Domain layer

Add a domain layer when logic is shared or complex.

From App architecture: layers and repositories

Which package do JPA annotations come from in Spring Boot 3 and later?Related

Answer: jakarta.persistence

The move to Jakarta EE renamed the packages.

From JPA and Spring Data repositories

Why return DTOs instead of JPA entities from controllers?Related

Answer: To avoid leaking internal fields and coupling the API to the database model

DTOs give a stable, safe API shape.

From Building a REST API

Which job belongs to the backend?Related

Answer: Checking that a note title is not empty before saving it

The backend applies the rules. The frontend can check too, but the backend must always check, because requests can be faked.

From What full-stack means

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Spring Boot basics” before its exam: JPA and Spring Data repositories.
  2. Module 11 exam: Spring Boot basics · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Design patterns, JVM internals and interviews. 60 topics left in the course before the final exam.

← Previous: Building a REST API