In-memory repository

In this lesson, we introduce a proper layered architecture by adding Service and Repository layers between the controllers and the data. Instead of returning hardcoded responses, controllers now delegate to services, which persist and retrieve data from thread-safe in-memory repositories backed by ConcurrentHashMap.
1.Create Model classes
These classes will be used to store data in in-memory repositories.
Author model
Represents an author entity with a unique UUID identifier and a name field.
7 collapsed lines
package com.romach007.spring_boot.model;
import lombok.Builder;import lombok.Data;
import java.util.UUID;
@Data@Builderpublic class Author { private UUID id; private String name;}Book model
Represents a book entity with fields for id, title, isbn, description, authors, and publicationDate.
9 collapsed lines
package com.romach007.spring_boot.model;
import lombok.Builder;import lombok.Data;
import java.time.LocalDate;import java.util.List;import java.util.UUID;
@Data@Builderpublic class Book { private UUID id; private String title; private String isbn; private String description; private List<UUID> authorIds; private LocalDate publicationDate;}2.Create Repositories
AuthorRepository
Interface defining CRUD operations for Author entities.
8 collapsed lines
package com.romach007.spring_boot.repository;
import com.romach007.spring_boot.model.Author;
import java.util.List;import java.util.Optional;import java.util.UUID;
public interface AuthorRepository { List<Author> findAll(); Optional<Author> findById(UUID id); Author save(Author author); void deleteById(UUID id);}BookRepository
Interface defining CRUD operations for Book entities.
8 collapsed lines
package com.romach007.spring_boot.repository;
import com.romach007.spring_boot.model.Book;
import java.util.List;import java.util.Optional;import java.util.UUID;
public interface BookRepository { List<Book> findAll(); Optional<Book> findById(UUID id); Book save(Book book); void deleteById(UUID id);}InMemoryAuthorRepository
Thread-safe in-memory implementation of AuthorRepository backed by ConcurrentHashMap.
11 collapsed lines
package com.romach007.spring_boot.repository;
import com.romach007.spring_boot.model.Author;import org.springframework.stereotype.Repository;
import java.util.ArrayList;import java.util.List;import java.util.Optional;import java.util.UUID;import java.util.concurrent.ConcurrentHashMap;
@Repositorypublic class InMemoryAuthorRepository implements AuthorRepository {
private final ConcurrentHashMap<UUID, Author> store = new ConcurrentHashMap<>();
@Override public List<Author> findAll() { return new ArrayList<>(store.values()); }
@Override public Optional<Author> findById(UUID id) { return Optional.ofNullable(store.get(id)); }
@Override public Author save(Author author) { store.put(author.getId(), author); return author; }
@Override public void deleteById(UUID id) { store.remove(id); }}InMemoryBookRepository
Thread-safe in-memory implementation of BookRepository backed by ConcurrentHashMap.
11 collapsed lines
package com.romach007.spring_boot.repository;
import com.romach007.spring_boot.model.Book;import org.springframework.stereotype.Repository;
import java.util.ArrayList;import java.util.List;import java.util.Optional;import java.util.UUID;import java.util.concurrent.ConcurrentHashMap;
@Repositorypublic class InMemoryBookRepository implements BookRepository {
private final ConcurrentHashMap<UUID, Book> store = new ConcurrentHashMap<>();
@Override public List<Book> findAll() { return new ArrayList<>(store.values()); }
@Override public Optional<Book> findById(UUID id) { return Optional.ofNullable(store.get(id)); }
@Override public Book save(Book book) { store.put(book.getId(), book); return book; }
@Override public void deleteById(UUID id) { store.remove(id); }}3.Create Services
AuthorService
Service layer for Author business logic; delegates persistence operations to AuthorRepository. Throws 404 Not Found when an author does not exist on get, update, or delete.
Possible responses
| Method | Response status code | Description |
|---|---|---|
list() | 200 OK | Get list of all authors |
get(id) | 200 OK | Get author by id |
| 404 NOT FOUND | Author not found | |
create(dto) | 201 CREATED | Create new author |
update(id, dto) | 200 OK | Update existing author |
| 404 NOT FOUND | Author not found | |
delete(id) | 204 NO CONTENT | Delete author |
| 404 NOT FOUND | Author not found |
14 collapsed lines
package com.romach007.spring_boot.service;
import com.romach007.spring_boot.dto.AuthorDTO;import com.romach007.spring_boot.dto.SaveAuthorDTO;import com.romach007.spring_boot.model.Author;import com.romach007.spring_boot.repository.AuthorRepository;import lombok.RequiredArgsConstructor;import org.springframework.http.HttpStatus;import org.springframework.stereotype.Service;import org.springframework.web.server.ResponseStatusException;
import java.util.List;import java.util.UUID;
@Service@RequiredArgsConstructorpublic class AuthorService {
private final AuthorRepository authorRepository;
public List<AuthorDTO> list() { return authorRepository.findAll().stream() .map(this::toDTO) .toList(); }
public AuthorDTO get(UUID id) { return authorRepository.findById(id) .map(this::toDTO) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Author not found: " + id)); }
public AuthorDTO create(SaveAuthorDTO dto) { Author author = Author.builder() .id(UUID.randomUUID()) .name(dto.getName()) .build(); return toDTO(authorRepository.save(author)); }
public AuthorDTO update(UUID id, SaveAuthorDTO dto) { Author author = authorRepository.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Author not found: " + id)); author.setName(dto.getName()); return toDTO(authorRepository.save(author)); }
public void delete(UUID id) { authorRepository.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Author not found: " + id)); authorRepository.deleteById(id); }
private AuthorDTO toDTO(Author author) { return new AuthorDTO(author.getId(), author.getName()); }}BookService
Service layer for Book business logic; delegates persistence operations to BookRepository. Throws 404 Not Found when a book does not exist, and 400 Bad Request when a provided authorId does not match any existing author.
Possible responses
| Method | Response status code | Description |
|---|---|---|
list() | 200 OK | Get list of all books |
get(id) | 200 OK | Get book by id |
| 404 NOT FOUND | Book not found | |
create(dto) | 201 CREATED | Create new book |
| 400 BAD REQUEST | Author not found | |
update(id, dto) | 200 OK | Update existing book |
| 404 NOT FOUND | Book not found | |
| 400 BAD REQUEST | Author not found | |
delete(id) | 204 NO CONTENT | Delete book |
| 404 NOT FOUND | Book not found |
15 collapsed lines
package com.romach007.spring_boot.service;
import com.romach007.spring_boot.dto.AuthorDTO;import com.romach007.spring_boot.dto.BookDTO;import com.romach007.spring_boot.dto.SaveBookDTO;import com.romach007.spring_boot.model.Book;import com.romach007.spring_boot.repository.BookRepository;import lombok.RequiredArgsConstructor;import org.springframework.http.HttpStatus;import org.springframework.stereotype.Service;import org.springframework.web.server.ResponseStatusException;
import java.util.List;import java.util.UUID;
@Service@RequiredArgsConstructorpublic class BookService {
private final BookRepository bookRepository; private final AuthorService authorService;
public List<BookDTO> list() { return bookRepository.findAll().stream() .map(this::toDTO) .toList(); }
public BookDTO get(UUID id) { return bookRepository.findById(id) .map(this::toDTO) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Book not found: " + id)); }
public BookDTO create(SaveBookDTO dto) { validateAuthorIds(dto.getAuthorIds()); Book book = Book.builder() .id(UUID.randomUUID()) .title(dto.getTitle()) .isbn(dto.getIsbn()) .description(dto.getDescription()) .authorIds(dto.getAuthorIds()) .publicationDate(dto.getPublicationDate()) .build(); return toDTO(bookRepository.save(book)); }
public BookDTO update(UUID id, SaveBookDTO dto) { Book book = bookRepository.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Book not found: " + id)); validateAuthorIds(dto.getAuthorIds()); book.setTitle(dto.getTitle()); book.setIsbn(dto.getIsbn()); book.setDescription(dto.getDescription()); book.setAuthorIds(dto.getAuthorIds()); book.setPublicationDate(dto.getPublicationDate()); return toDTO(bookRepository.save(book)); }
public void delete(UUID id) { bookRepository.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Book not found: " + id)); bookRepository.deleteById(id); }
private void validateAuthorIds(List<UUID> authorIds) { for (UUID authorId : authorIds) { try { authorService.get(authorId); } catch (ResponseStatusException e) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Author not found: " + authorId); } } }
private BookDTO toDTO(Book book) { List<AuthorDTO> authors = book.getAuthorIds().stream() .map(authorService::get) .toList(); return new BookDTO( book.getId(), book.getTitle(), book.getIsbn(), book.getDescription(), authors, book.getPublicationDate() ); }}4.Update Controllers
AuthorController
Updated to inject AuthorService and delegate CRUD operations instead of returning hardcoded responses.
package com.romach007.spring_boot.controller;
import com.romach007.spring_boot.dto.AuthorDTO;import com.romach007.spring_boot.dto.SaveAuthorDTO;import com.romach007.spring_boot.service.AuthorService;import lombok.RequiredArgsConstructor;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.*;
import java.util.List;import java.util.UUID;
@RestController@RequestMapping("/api/authors")@RequiredArgsConstructorpublic class AuthorController {
private final AuthorService authorService;
@GetMapping public List<AuthorDTO> list() { return List.of( new AuthorDTO(UUID.fromString("c77eff4d-d6c0-4386-9c8e-9e6e7b0d9c55"), "John Doe") ); return authorService.list(); }
@GetMapping("/{id}") public AuthorDTO get(@PathVariable UUID id) { return new AuthorDTO(id, "John Doe"); return authorService.get(id); }
@PostMapping @ResponseStatus(HttpStatus.CREATED) public AuthorDTO create(@RequestBody SaveAuthorDTO dto) { return new AuthorDTO(UUID.randomUUID(), dto.getName()); return authorService.create(dto); }
@PutMapping("/{id}") public AuthorDTO update(@PathVariable UUID id, @RequestBody SaveAuthorDTO dto) { return new AuthorDTO(id, dto.getName()); return authorService.update(id, dto); }
@DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void delete(@PathVariable UUID id) {} public void delete(@PathVariable UUID id) { authorService.delete(id); }}BookController
Updated to inject BookService and delegate CRUD operations instead of returning hardcoded responses.
package com.romach007.spring_boot.controller;
import com.romach007.spring_boot.dto.AuthorDTO;import com.romach007.spring_boot.dto.BookDTO;import com.romach007.spring_boot.dto.SaveBookDTO;import com.romach007.spring_boot.service.BookService;import lombok.RequiredArgsConstructor;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;import java.util.List;import java.util.UUID;
@RestController@RequestMapping("/api/books")@RequiredArgsConstructorpublic class BookController {
private final BookService bookService;
@GetMapping public List<BookDTO> list() { return List.of( new BookDTO( UUID.fromString("7acf89b7-7aaf-4a8c-bb98-1d85ea7d0e96"), "Book Title", "978-3-16-148410-0", "description", List.of( new AuthorDTO(UUID.fromString("c77eff4d-d6c0-4386-9c8e-9e6e7b0d9c55"), "John Doe") ), LocalDate.parse("2007-12-03") ) ); return bookService.list(); }
@GetMapping("/{id}") public BookDTO get(@PathVariable UUID id) { return new BookDTO( id, "Book Title", "978-3-16-148410-0", "description", List.of( new AuthorDTO(UUID.fromString("c77eff4d-d6c0-4386-9c8e-9e6e7b0d9c55"), "John Doe") ), LocalDate.parse("2007-12-03") ); return bookService.get(id); }
@PostMapping @ResponseStatus(HttpStatus.CREATED) public BookDTO create(@RequestBody SaveBookDTO dto) { return new BookDTO( UUID.randomUUID(), dto.getTitle(), dto.getIsbn(), dto.getDescription(), dto.getAuthorIds().stream().map(id -> new AuthorDTO(id, "John Doe")).toList(), dto.getPublicationDate() ); return bookService.create(dto); }
@PutMapping("/{id}") public BookDTO update(@PathVariable UUID id, @RequestBody SaveBookDTO dto) { return new BookDTO( id, dto.getTitle(), dto.getIsbn(), dto.getDescription(), dto.getAuthorIds().stream() .map(authorId -> new AuthorDTO(authorId, "John Doe")) .toList(), dto.getPublicationDate() ); return bookService.update(id, dto); }
@DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void delete(@PathVariable UUID id) {} public void delete(@PathVariable UUID id) { bookService.delete(id); }}