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.
@SpringBootTest(
classes = TestDataDomainEventsApplication.class,
properties = "spring.jpa.hibernate.ddl-auto=create",
webEnvironment = RANDOM_PORT
)
class BookPurchaseTests {
@Autowired
private BookAvailabilityRepository availabilities;
@Autowired
private BookRepository books;
@Autowired
private TestRestTemplate client;
private Book book;
@BeforeEach
void setup() {
book = books.save(book());
availabilities.save(availability());
}
@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);
}
private Book book() {
var book = new Book();
book.setTitle("Say Nothing: A True Story of Murder and Memory in Northern Ireland");
book.setAuthor("Patrick Radden Keefe");
book.setIsbn(9780385543378L);
return book;
}
private BookAvailability availability() {
var availability = new BookAvailability();
availability.setIsbn(9780385543378L);
availability.setTotal(100);
return availability;
}
}
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;
// getter & setter are 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.