C

In-memory repository

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.

src/main/java/com/romach007/spring_boot/model/Author.java
7 collapsed lines
package com.romach007.spring_boot.model;
import lombok.Builder;
import lombok.Data;
import java.util.UUID;
@Data
@Builder
public class Author {
private UUID id;
private String name;
}

Book model

Represents a book entity with fields for id, title, isbn, description, authors, and publicationDate.

src/main/java/com/romach007/spring_boot/model/Book.java
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
@Builder
public 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.

src/main/java/com/romach007/spring_boot/repository/AuthorRepository.java
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.

src/main/java/com/romach007/spring_boot/repository/BookRepository.java
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.

src/main/java/com/romach007/spring_boot/repository/InMemoryAuthorRepository.java
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;
@Repository
public 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.

src/main/java/com/romach007/spring_boot/repository/InMemoryBookRepository.java
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;
@Repository
public 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

MethodResponse status codeDescription
list()200 OKGet list of all authors
get(id)200 OKGet author by id
404 NOT FOUNDAuthor not found
create(dto)201 CREATEDCreate new author
update(id, dto)200 OKUpdate existing author
404 NOT FOUNDAuthor not found
delete(id)204 NO CONTENTDelete author
404 NOT FOUNDAuthor not found
src/main/java/com/romach007/spring_boot/service/AuthorService.java
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
@RequiredArgsConstructor
public 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

MethodResponse status codeDescription
list()200 OKGet list of all books
get(id)200 OKGet book by id
404 NOT FOUNDBook not found
create(dto)201 CREATEDCreate new book
400 BAD REQUESTAuthor not found
update(id, dto)200 OKUpdate existing book
404 NOT FOUNDBook not found
400 BAD REQUESTAuthor not found
delete(id)204 NO CONTENTDelete book
404 NOT FOUNDBook not found
src/main/java/com/romach007/spring_boot/service/BookService.java
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
@RequiredArgsConstructor
public 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.

controllers/src/main/java/com/romach007/spring_boot/controller/AuthorController.java vs in-memory-repository/src/main/java/com/romach007/spring_boot/controller/AuthorController.java
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")
@RequiredArgsConstructor
public 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.

controllers/src/main/java/com/romach007/spring_boot/controller/BookController.java vs in-memory-repository/src/main/java/com/romach007/spring_boot/controller/BookController.java
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")
@RequiredArgsConstructor
public 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);
}
}