Skip to content

Commit ff32795

Browse files
committed
feat(architecture): introduce anti-corruption layer between domains
- Create AuthorPort interface and AuthorAdapter - Create CategoryPort interface and CategoryAdapter - Create BookAvailabilityPort interface and BookRepositoryAdapter - Create UserLookupService interface and UserLookupServiceImpl - Create BookMetricsConfig desacoplar métricas de lógica de negócio - Refactor BookService to use AuthorLookupService and CategoryLookupService instead of direct repository injection - Refactor LoanService to use BookLookupService and UserLookupService instead of direct repository injection
1 parent 81c65b2 commit ff32795

File tree

14 files changed

+204
-57
lines changed

14 files changed

+204
-57
lines changed

readme.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
![PDF](https://github.com/erichiroshi/library-api/actions/workflows/readme-pdf.yml/badge.svg)
55
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=library-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=library-api)
66
[![codecov](https://codecov.io/github/erichiroshi/library-api/graph/badge.svg?token=Y71AMP148X)](https://codecov.io/github/erichiroshi/library-api)
7+
78
![Java](https://img.shields.io/badge/Java-25-red)
8-
![Spring Boot](https://img.shields.io/badge/Spring%20Boot-4.x-brightgreen)
9-
![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-blue)
9+
![Spring Boot 4](https://img.shields.io/badge/Spring%20Boot-4.x-brightgreen)
10+
![PostgreSQL 16](https://img.shields.io/badge/PostgreSQL-16-blue)
1011
![Redis](https://img.shields.io/badge/Redis-Cache-red)
1112
![Docker](https://img.shields.io/badge/Docker-Enabled-blue)
12-
![AWS S3](https://img.shields.io/badge/AWS-S3-orange)
13+
![Observability](https://img.shields.io/badge/Observability-OpenTelemetry%20%2B%20Zipkin-purple)
14+
![Resilience](https://img.shields.io/badge/Resilience-Resilience4j-orange)
15+
![AWS S3](https://img.shields.io/badge/AWS-S3-black)
1316

1417
Backend production-ready projetado com foco em previsibilidade, observabilidade e isolamento de responsabilidades.
1518

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.example.library.author;
2+
3+
import java.util.Collection;
4+
import java.util.HashSet;
5+
import java.util.Set;
6+
7+
import org.springframework.stereotype.Service;
8+
import org.springframework.transaction.annotation.Transactional;
9+
10+
import lombok.RequiredArgsConstructor;
11+
12+
@Service
13+
@RequiredArgsConstructor
14+
public class AuthorAdapter implements AuthorPort {
15+
16+
private final AuthorRepository repository;
17+
18+
@Override
19+
@Transactional(readOnly = true)
20+
public Set<Author> findAllById(Collection<Long> ids) {
21+
return new HashSet<>(repository.findAllById(ids));
22+
}
23+
24+
@Override
25+
@Transactional(readOnly = true)
26+
public int countByIds(Collection<Long> ids) {
27+
return repository.findAllById(ids).size();
28+
}
29+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.example.library.author;
2+
3+
import java.util.Collection;
4+
import java.util.Set;
5+
6+
public interface AuthorPort {
7+
Set<Author> findAllById(Collection<Long> ids);
8+
9+
int countByIds(Collection<Long> ids);
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.library.book;
2+
3+
import java.util.Optional;
4+
5+
public interface BookAvailabilityPort {
6+
Optional<Book> findById(Long id);
7+
8+
int decrementCopies(Long id);
9+
10+
void restoreCopies(Long id, int quantity);
11+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.example.library.book;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
6+
import io.micrometer.core.instrument.Counter;
7+
import io.micrometer.core.instrument.MeterRegistry;
8+
9+
@Configuration
10+
class BookMetricsConfig {
11+
12+
@Bean
13+
Counter bookCreatedCounter(MeterRegistry registry) {
14+
return Counter.builder("library.books.created")
15+
.description("Quantidade de livros criados")
16+
.register(registry);
17+
}
18+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.example.library.book;
2+
3+
import java.util.Optional;
4+
5+
import org.springframework.stereotype.Service;
6+
import org.springframework.transaction.annotation.Transactional;
7+
8+
import lombok.RequiredArgsConstructor;
9+
10+
@Service
11+
@RequiredArgsConstructor
12+
public class BookRepositoryAdapter implements BookAvailabilityPort {
13+
14+
private final BookRepository repository;
15+
16+
@Override
17+
@Transactional(readOnly = true)
18+
public Optional<Book> findById(Long id) {
19+
return repository.findById(id);
20+
}
21+
22+
@Override
23+
@Transactional
24+
public int decrementCopies(Long id) {
25+
return repository.decrementCopies(id);
26+
}
27+
28+
@Override
29+
@Transactional
30+
public void restoreCopies(Long id, int quantity) {
31+
repository.findById(id).ifPresent(book ->
32+
book.setAvailableCopies(book.getAvailableCopies() + quantity)
33+
);
34+
}
35+
}

src/main/java/com/example/library/book/BookService.java

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@
1818
import org.springframework.web.multipart.MultipartFile;
1919

2020
import io.micrometer.core.instrument.Counter;
21-
import io.micrometer.core.instrument.MeterRegistry;
2221

2322
import com.example.library.author.Author;
24-
import com.example.library.author.AuthorRepository;
23+
import com.example.library.author.AuthorPort;
2524
import com.example.library.aws.S3Service;
2625
import com.example.library.book.dto.BookCreateDTO;
2726
import com.example.library.book.dto.BookResponseDTO;
@@ -30,42 +29,32 @@
3029
import com.example.library.book.exception.InvalidOperationException;
3130
import com.example.library.book.mapper.BookMapper;
3231
import com.example.library.category.Category;
33-
import com.example.library.category.CategoryRepository;
32+
import com.example.library.category.CategoryPort;
3433
import com.example.library.category.exception.CategoryNotFoundException;
3534
import com.example.library.common.config.delay_cache_test.ArtificialDelayService;
3635
import com.example.library.common.dto.PageResponseDTO;
3736

37+
import lombok.RequiredArgsConstructor;
38+
39+
@RequiredArgsConstructor
3840
@Service
3941
public class BookService {
4042

4143
private static final Logger log = LoggerFactory.getLogger(BookService.class);
4244
private static final String S3_FOLDER_NAME = "books/";
4345

4446
private final BookRepository repository;
45-
private final AuthorRepository authorRepository;
46-
private final CategoryRepository categoryRepository;
47+
private final AuthorPort authorPort;
48+
private final CategoryPort categoryPort;
4749
private final BookMapper mapper;
4850
private final S3Service s3Service;
51+
4952
private final ArtificialDelayService delayService;
5053
private final Counter bookCreatedCounter;
5154

5255
@Value("${img.prefix.book}")
5356
private String prefix;
5457

55-
public BookService(BookRepository repository, AuthorRepository authorRepository,
56-
CategoryRepository categoryRepository, BookMapper bookMapper, S3Service s3Service, MeterRegistry registry, ArtificialDelayService delayService) {
57-
58-
this.repository = repository;
59-
this.authorRepository = authorRepository;
60-
this.categoryRepository = categoryRepository;
61-
this.mapper = bookMapper;
62-
this.s3Service = s3Service;
63-
this.delayService = delayService;
64-
this.bookCreatedCounter = Counter.builder("library.books.created")
65-
.description("Quantidade de livros criados")
66-
.register(registry);
67-
}
68-
6958
@CacheEvict(value = "books", allEntries = true)
7059
@Transactional
7160
public BookResponseDTO create(BookCreateDTO dto) {
@@ -81,9 +70,9 @@ public BookResponseDTO create(BookCreateDTO dto) {
8170
Book book = mapper.toEntity(dto);
8271
log.info("Creating book: {}", book.getTitle());
8372

84-
Set<Author> authors = new HashSet<>(authorRepository.findAllById(dto.authorIds()));
73+
Set<Author> authors = new HashSet<>(authorPort.findAllById(dto.authorIds()));
8574

86-
Category category = categoryRepository.findById(dto.categoryId())
75+
Category category = categoryPort.findById(dto.categoryId())
8776
.orElseThrow(() -> new CategoryNotFoundException(dto.categoryId()));
8877

8978
if (authors.size() != dto.authorIds().size()) {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.example.library.category;
2+
3+
import java.util.Optional;
4+
5+
import org.springframework.stereotype.Service;
6+
import org.springframework.transaction.annotation.Transactional;
7+
8+
import lombok.RequiredArgsConstructor;
9+
10+
@Service
11+
@RequiredArgsConstructor
12+
public class CategoryAdapter implements CategoryPort {
13+
14+
private final CategoryRepository repository;
15+
16+
@Override
17+
@Transactional(readOnly = true)
18+
public Optional<Category> findById(Long id) {
19+
return repository.findById(id);
20+
}
21+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.example.library.category;
2+
3+
import java.util.Optional;
4+
5+
public interface CategoryPort {
6+
Optional<Category> findById(Long id);
7+
}

src/main/java/com/example/library/loan/LoanService.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import org.springframework.transaction.annotation.Transactional;
1515

1616
import com.example.library.book.Book;
17-
import com.example.library.book.BookRepository;
17+
import com.example.library.book.BookAvailabilityPort;
1818
import com.example.library.book.exception.BookNotFoundException;
1919
import com.example.library.loan.dto.LoanCreateDTO;
2020
import com.example.library.loan.dto.LoanResponseDTO;
@@ -27,7 +27,7 @@
2727
import com.example.library.loan.exception.LoanUnauthorizedException;
2828
import com.example.library.loan.mapper.LoanMapper;
2929
import com.example.library.user.User;
30-
import com.example.library.user.UserRepository;
30+
import com.example.library.user.UserLookupService;
3131
import com.example.library.user.exception.UserNotFoundException;
3232

3333
import lombok.RequiredArgsConstructor;
@@ -39,8 +39,8 @@ public class LoanService {
3939
private static final Logger log = LoggerFactory.getLogger(LoanService.class);
4040

4141
private final LoanRepository loanRepository;
42-
private final BookRepository bookRepository; // usado apenas para verificação e decrement atômico na criação
43-
private final UserRepository userRepository;
42+
private final BookAvailabilityPort bookAvailabilityPort; // usado apenas para verificação e decrement atômico na criação
43+
private final UserLookupService userLookupService; // usado para validar existência de usuários em consultas administrativas
4444
private final LoanMapper mapper;
4545
private final ApplicationEventPublisher eventPublisher;
4646

@@ -55,7 +55,7 @@ public LoanResponseDTO create(LoanCreateDTO dto) {
5555

5656
// Valida existência de todos os livros antes de iniciar
5757
for (Long bookId : dto.booksId()) {
58-
bookRepository.findById(bookId)
58+
bookAvailabilityPort.findById(bookId)
5959
.orElseThrow(() -> new BookNotFoundException(bookId));
6060
}
6161

@@ -68,11 +68,11 @@ public LoanResponseDTO create(LoanCreateDTO dto) {
6868
loan.setStatus(LoanStatus.WAITING_RETURN);
6969

7070
for (Long bookId : dto.booksId()) {
71-
Book book = bookRepository.findById(bookId)
71+
Book book = bookAvailabilityPort.findById(bookId)
7272
.orElseThrow(() -> new BookNotFoundException(bookId));
7373

7474
// Update atômico — evita race condition em empréstimos concorrentes
75-
int updated = bookRepository.decrementCopies(bookId);
75+
int updated = bookAvailabilityPort.decrementCopies(bookId);
7676
if (updated == 0) {
7777
throw new BookNotAvailableException(bookId, book.getTitle());
7878
}
@@ -211,7 +211,7 @@ public List<LoanResponseDTO> findMyLoans() {
211211

212212
@Transactional(readOnly = true)
213213
public List<LoanResponseDTO> findByUser(Long userId) {
214-
userRepository.findById(userId)
214+
userLookupService.findById(userId)
215215
.orElseThrow(() -> new UserNotFoundException(userId));
216216
return loanRepository.findByUserIdWithItems(userId)
217217
.stream()

0 commit comments

Comments
 (0)