Reduce method complexity by utilising @DomainEvents from Spring Data JPA.

Background

In this repository we will explore Spring Data JPA helps us to adhere to Single Responsibility, a component of SOLID Principles.

We will reduce responsibilities of a method that does more than one thing.

Scenario

This repository demonstrates a scenario where once a book is purchased, its total availability will be reduced.

Implementation

Integration End-to-end Test

In the spirit of TDD, we will start by implementing an integration end-to-end test.

class BookPurchaseTests {

    @Autowired
    private BookAvailabilityRepository availabilities;

    @Autowired
    private BookRepository books;

    @Autowired
    private TestRestTemplate client;

    @Test
    @DisplayName("Given total book availability is 100 When a book is purchased Then total book availability should be 99")
    void purchase() {
        client.delete("/books/{id}/purchase", book.getId());

        var availability = availabilities.findByIsbn(book.getIsbn());

        assertThat(availability).get()
                .extracting("total")
                .isEqualTo(99);
    }

}

Full implementation can be found in BookPurchaseTests.java.

Domain and Repository class

Our domain class, Book.java, will hold information about the event that will be published.

@Entity
public class Book extends AbstractAggregateRoot<Book> {

    @Id
    @GeneratedValue
    private Long id;
    private String title;
    private String author;
    private Long isbn;

    // omitted for brevity

    public Book purchase() {
        registerEvent(new BookPurchaseEvent(this));
        return this;
    }

}

The class will publish BookPurchaseEvent.java when a book is purchased.

Next is to implement a repository classes for Book and BookAvailability.java.

public interface BookRepository extends JpaRepository<Book, Long> {
}
interface BookAvailabilityRepository extends JpaRepository<BookAvailability, Long> {

    Optional<BookAvailability> findByIsbn(Long isbn);

}

REST Resource Class

BookResource is a typical @RestController class which will trigger Book.purchase.

@RestController
class BookResource {

    private final BookRepository repository;

    BookResource(BookRepository repository) {
        this.repository = repository;
    }

    @Transactional
    @DeleteMapping("/books/{id}/purchase")
    public void purchase(@PathVariable Long id) {
        repository.findById(id).map(Book::purchase).ifPresent(repository::delete);
    }

}

Event Listener Class

Finally, we will implement a @Service class that will observe BookPurchaseEvent and reduce the total availability of the book.

@Service
class BookAvailabilityManagement {

    private final BookAvailabilityRepository repository;

    BookAvailabilityManagement(BookAvailabilityRepository repository) {
        this.repository = repository;
    }

    @TransactionalEventListener
    @Transactional(propagation = REQUIRES_NEW)
    public void updateTotal(BookPurchaseEvent event) {
        var book = event.getSource();

        repository.findByIsbn(book.getIsbn())
                .map(BookAvailability::reduceTotal)
                .ifPresent(repository::save);
    }

Verification

By executing BookPurchaseTests.purchase, we will see that the test passes.