The Web & Data Layers
Build the REST API and make it durable: controllers and DTOs with validation and ProblemDetail, then JPA entities, Flyway migrations, and transactional lending.
On this page
With a domain in place, BookVault needs two things to be useful: a way for clients to reach it (the web layer) and a way to remember data across restarts (the data layer). This lesson builds both, and shows how they meet in the service.
The web layer: controllers, DTOs, validation
A @RestController maps HTTP to methods. The key discipline: never expose entities directly -
accept and return DTOs, so your API contract is separate from your internal model:
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService books;
public BookController(BookService books) { this.books = books; }
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public BookResponse create(@Valid @RequestBody CreateBookRequest request) {
return BookResponse.from(books.add(request));
}
}@Valid triggers Bean Validation on the request DTO (a record with @NotBlank, @Positive, ...),
so bad input is rejected before it reaches your logic. And when something goes wrong, BookVault
returns a ProblemDetail (RFC 9457) - a standard, machine-readable error - via a
@RestControllerAdvice, instead of a stack trace:
@ExceptionHandler(BookNotFoundException.class)
ProblemDetail handleNotFound(BookNotFoundException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}The data layer: entities, repositories, migrations
Now make it durable. Annotate the domain objects as JPA entities, and declare a Spring Data repository - an interface Spring implements for you:
public interface BookRepository extends JpaRepository<Book, Long> {
Optional<Book> findByIsbn(String isbn); // derived query - no SQL
List<Book> findByAuthorContainingIgnoreCase(String author);
}The schema is owned by Flyway, not Hibernate's auto-DDL: versioned SQL migrations
(V1__create_schema.sql, ...) run on startup, so the database's shape is explicit, reviewable, and
reproducible. Hibernate is set to validate - it checks the entities match the schema, never changes
it.
Where they meet: the transactional service
The service ties the layers together and enforces the business rules atomically. Borrowing is the showcase - two writes that must both succeed or both fail:
@Transactional
public Loan borrow(String isbn, Long memberId) {
Book book = books.findByIsbn(isbn).orElseThrow(() -> new BookNotFoundException(isbn));
Member member = members.findById(memberId).orElseThrow(() -> new MemberNotFoundException(memberId));
book.reserveCopy(); // UPDATE: one fewer copy
return loans.save(new Loan(book, member, today, today.plusDays(properties.loanDays()))); // INSERT
// if the INSERT fails, the reserveCopy() UPDATE is rolled back too - atomicity
}Watch the N+1 and lazy loading
With open-in-view: false (the right production setting), a lazy association accessed outside the
transaction throws. BookVault fixes this deliberately - JOIN FETCH in the loan queries loads a loan
with its book and member in one query, avoiding both LazyInitializationException and the N+1
problem. Persistence rewards paying attention to how data is fetched, not just what.
The menu (your DTOs and endpoints) is the contract diners see - stable, curated, hiding the kitchen's mess. The kitchen (entities, repositories, SQL) is where the real work happens, and it can be reorganized without reprinting the menu. Keeping DTOs separate from entities is that separation: clients depend on the menu, so you're free to change the kitchen.
It's tempting to have BookController return the Book JPA entity straight from the repository,
skipping BookResponse. Give two concrete problems this causes, and explain how a DTO plus Flyway
migrations keep BookVault's API and schema evolving safely.
How do BookVault's web and data layers stay decoupled and correct?
Key takeaways
- A @RestController maps HTTP to methods; accept and return DTOs so the API contract stays separate from entities.
- @Valid enforces Bean Validation on request DTOs; a @RestControllerAdvice returns standard ProblemDetail errors.
- JPA entities plus Spring Data repositories give persistence with derived queries and no boilerplate SQL.
- Flyway owns the schema via versioned migrations (Hibernate only validates), keeping the database explicit and reproducible.
- The service unites the layers and makes multi-step writes atomic with @Transactional - borrow reserves a copy and records a loan together or not at all.
- With open-in-view false, use JOIN FETCH to load associations in one query, avoiding lazy-loading errors and N+1.