Spring Data — Power of Domain Events
Tags: tutorial, architecture, design, eventdriven
Published: 2021-11-27 09:17:00

[Domain Event](https://martinfowler.com/eaaDev/DomainEvent.html) is one of the ideas from [Domain Driven Design](https://en.wikipedia.org/wiki/Domain-driven_design). Once you become familiar with the technique, you won't be willing to deal without it anymore. So, in this article, I'm showing you an example of application development. We're doing the process step by step as the new requirements come. That shall give us a clear understanding of the Domain Events valueness.

Our stack is Java 11 + Spring Boot + Hibernate.

![cover_image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q8pplf39k1yd78dce0kr.png)

Assuming we're creating the books selling service. Authors can place their books on sale, whilst customers can buy them.

Let's define the primary business entities. The `Book` itself.

```java
@Entity
@Table
public class Book {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;
  
  private String name;

  private String description;
  
  private OffsetDateTime dateCreated;
  
  private OffsetDateTime lastDateUpdated;
  
  @ManyToOne(fetch = LAZY)
  @JoinColumn(name = "author_id")
  private Author author;

  private int price;

  @OneToMany(fetch = LAZY, mappedBy = "book", cascade = ALL)
  private List<BookSale> bookSales = new ArrayList<>();

  // getters, setters
}
```

And the `BookSale`.

```java
@Entity
@Table
public class BookSale {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;

  private int priceSold;

  private OffsetDateTime dateSold;

  @ManyToOne(fetch = LAZY)
  @JoinColumn(name = "book_id")
  private Book book;

  // getters, setters
}
```

A `Book` instance has the name, author, date of creation, last date of update, price and the list of all sales.

> For the sake of simplicity, we assume that books have a single author and all the prices have the same currency.

Ok, the minimum domain model is grounded. It's time to implement the business requirements.

## 1. Each book sell should be registered
That's the whole idea of our system.

Here is the first attempt.

```java
@Service
public class BookSaleServiceImpl implements BookSaleService {
  private final BookRepository bookRepository;
  private final BookSaleRepository bookSaleRepository;

  @Override
  @Transactional
  public void sellBook(Long bookId) {
    final var book =
        bookRepository.findById(bookId)
            .orElseThrow(() -> new NoSuchElementException(
                "Book is not found"
            ));
    BookSale bookSale = new BookSale();
    bookSale.setBook(book);
    bookSale.setDateSold(OffsetDateTime.now());
    bookSale.setPriceSold(book.getPrice());
    bookSaleRepository.save(bookSale);
  }
}
```

If you work with Spring regularly, you have probably seen similar code snippets many times. The design architecture that we have grounded here can be described as [Anemic Domain Model](https://martinfowler.com/bliki/AnemicDomainModel.html). It means that we place the vast majority of the business logic inside the service layer. Whilst the entities act just as simple data structures with getters and setters.

Many authors classify this style as an anti-pattern. But why is that? This approach seems natural, isn't it? Besides, the business logic is not so complex in our case. Well, there are no problems in such a primitive example. At least, for now.

## 2. An author should be notified with every 100 of their book sales

We want to let the author know that their books are being sold.

How do we implement the feature? Well, the naive approach is to put functionality inside the `sellBook` method.

```java
@Service
public class BookSaleServiceImpl implements BookSaleService {
  private final BookRepository bookRepository;
  private final BookSaleRepository bookSaleRepository;
  private final EmailService emailService;

  @Override
  @Transactional
  public void sellBook(Long bookId) {
    final var book =
        bookRepository.findById(bookId)
            .orElseThrow(() -> new NoSuchElementException(
                "Book is not found"
            ));
    BookSale bookSale = new BookSale();
    bookSale.setBook(book);
    bookSale.setDateSold(OffsetDateTime.now());
    bookSale.setPriceSold(book.getPrice());
    bookSaleRepository.save(bookSale);

    int totalSoldBooks = book.getBookSales().size();
    if (totalSoldBooks % 100 == 0) {
      Author author = book.getAuthor();
      emailService.send(author.getEmail(), "Another 100 books of yours have been sold!");
    }
  }
}
```
The first thing that appears is that the transaction is still being processed when the `emailService.send` method is called. Firstly, it's a performance penalty. Secondly, there is a probability that the transaction shall be rolled back in the end. In that case, we don't want to send any emails.

We can fix that by applying [programmatic transactions](https://www.baeldung.com/spring-programmatic-transaction-management).

```java
@Service
public class BookSaleServiceImpl implements BookSaleService {
  private final BookRepository bookRepository;
  private final BookSaleRepository bookSaleRepository;
  private final EmailService emailService;
  private final TransactionTemplate transactionTemplate;

  @Override
  public void sellBook(Long bookId) {
    final var savedBook = transactionTemplate.execute(status -> {
      final var book =
          bookRepository.findById(bookId)
              .orElseThrow(() -> new NoSuchElementException(
                  "Book is not found"
              ));
      BookSale bookSale = new BookSale();
      bookSale.setBook(book);
      bookSale.setDateSold(OffsetDateTime.now());
      bookSale.setPriceSold(book.getPrice());
      bookSaleRepository.save(bookSale);
      return book;
    });

    int totalSoldBooks = savedBook.getBookSales().size();
    if (totalSoldBooks % 100 == 0) {
      Author author = savedBook.getAuthor();
      emailService.send(author.getEmail(), "Another 100 books of your have been sold!");
    }
  }
}
```

But one issue remains. This approach breaks [Single-Responsibility Principle (SRP)](https://en.wikipedia.org/wiki/Single-responsibility_principle) and [Open-Closed Principle (OCP)](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle#:~:text=In%20object%2Doriented%20programming%2C%20the,without%20modifying%20its%20source%20code.). The better option is the [Decorator Pattern](https://en.wikipedia.org/wiki/Decorator_pattern).

```java
@Service
public class EmailNotifierBookSaleService implements BookSaleService {
  @ActualBookSaleServiceQualifier
  private final BookSaleService origin;
  private final BookRepository bookRepository;
  private final EmailService emailService;

  @Override
  public void sellBook(Long bookId) {
    origin.sellBook(bookId);
    final var savedBook = bookRepository.findById(bookId).orElseThrow();
    int totalSoldBooks = savedBook.getBookSales().size();
    if (totalSoldBooks % 100 == 0) {
      Author author = savedBook.getAuthor();
      emailService.send(author.getEmail(), "Another 100 books of your have been sold!");
    }
  }
}
```

`EmailNotifierBookSaleService` injects `BookSaleService` interface. In production environment, this one is going to be the `BookSaleServiceImpl` implementation (the [Qualifier annotation](https://www.baeldung.com/spring-qualifier-annotation) points to that one). But in the test environment, we could use a stub or a mock.

That does look much better. The functionality is split between two services. And each of them can be tested individually. 

## 3. Each book update should be archived

Analysts have decided that every possible book update (including book sale) should be archived. Here is the `BookArchive` entity.

```java
@Entity
@Table
public class BookArchive {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;

  private String name;

  private String description;

  private int soldBooks;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "author_id")
  private Author author;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "book_id")
  private Book book;

  private OffsetDateTime dateArchiveCreated;

  private OffsetDateTime lastDateVersionUpdated;

  // getters, setters
}
```

How do we track book sales? Well, we could add the functionality directly to `BookSaleServiceImpl` but we have already pointed out that it's a bad way. So, another decorator comes in.

```java
@Service
public class ArchiveBookSaleService implements BookSaleService {
  @ActualBookSaleServiceQualifier
  private final BookSaleService origin;
  private final BookRepository bookRepository;
  private final BookArchiveRepository bookArchiveRepository;

  @Override
  @Transactional
  public void sellBook(Long bookId) {
    Book book = bookRepository.findById(bookId).orElseThrow();

    BookArchive bookArchive = new BookArchive();
    bookArchive.setBook(book);
    bookArchive.setName(book.getName());
    bookArchive.setDescription(book.getDescription());
    bookArchive.setDateArchiveCreated(OffsetDateTime.now());
    bookArchive.setLastDateVersionUpdated(requireNonNullElse(book.getLastDateUpdated(), book.getDateCreated()));
    bookArchive.setSoldBooks(book.getBookSales().size());
    
    bookArchiveRepository.save(bookArchive);
    
    origin.sellBook(bookId);
  }
}
```

Some important details should be pointed out.
The `sellBook` method is wrapped with `@Transactional`. The reason is that an archived record should be created in the same transaction as the `BookSale` itself. If the main operation fails, we don't want to store any archives.

> The `bookRepository.findById(id)` method is called two times during the execution. But since there is one transaction, Hibernate returns [the cached instance](https://vladmihalcea.com/jpa-hibernate-first-level-cache/) from the persistence context on the second call. So, there are no additional database round-trips.

The second point is the `@ActualBookSaleServiceQualifier`. `EmailNotifierBookSaleService` does not start any transaction. It means that `origin` has to be of type `BookSaleServiceImpl`. Therefore we have to edit `EmailNotifierBookSaleService` in order not to inject `BookSaleServiceImpl` twice.

So, here is the schema of the current process.

![book sale flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y1mnwag7v2orwkhybsl5.png)

If the system is not complex, this approach might be sufficient. But the Book Selling Application can be a huge enterprise solution. See, we have only started the development but there are already two decorators. Besides, the order of wrapping matters as well. That's why we had to change *qualifiers*.

It seems a bit overcomplicated, isn't it? Well, this is not the end.

## 4. Administrators should be able to update a book's name and description. Authors should be notified of every update by email

That does make sense as well. For example, there might be typos. The requirement can be split into three different functionalities:

1. Book info update
1. Book archiving
1. Notifying by email

But here is the thing. If we keep following the same approach as before, there will be the primary service with business logic and two additional decorators. Déjà vu, isn't it? Every time the new requirement comes in we have to wrap the service layer with new decorators. What's the problem? Well, some of the previously implemented functionality has to be repeated. For example, book archiving. No matter what exactly happened to the book, the new archive record should be created. Because that's what analysts need. Emails are also the case. The difference is the frequency of their sendings.

So, what's the better solution? That is the moment when Domain Events come in. But first, we have to do some refactoring.

## No Anemic Domain Model

What kind of requests do we have so far? Only two of them. A book sale request and its info updating. Let's rewrite the `Book` entity a bit.

```java
@Entity
@Table
public class Book {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;

  private String name;

  private String description;

  private OffsetDateTime dateCreated;

  private OffsetDateTime lastDateUpdated;

  @ManyToOne(fetch = LAZY)
  @JoinColumn(name = "author_id")
  private Author author;

  private int price;

  @OneToMany(fetch = LAZY, mappedBy = "book", cascade = ALL)
  private List<BookSale> bookSales = new ArrayList<>();

  public void sell() {
    final var bookSale = new BookSale();
    bookSale.setBook(this)
    bookSale.setDateSold(OffsetDateTime.now())
    bookSale.setPriceSold(price);
    bookSales.add(bookSale);
  }

  public void changeInfo(String name, String description) {
    this.name = name;
    this.description = description;
    lastDateUpdated = OffsetDateTime.now();
  }
}
```
I want to pay attention to `sell` and `changeInfo` methods. The first one registers a new book sale. And the second one updates the book's name and description. 

Seems that nothing has changed so far. We just combined the functionality that could be performed via setters calling. Well, it's true. But let's move forward. Now let's refactor the `BookSaleServiceImpl`.

```java
@Service
public class BookSaleServiceImpl implements BookSaleService {
  private final BookRepository bookRepository;

  @Transactional
  @Override
  public void sellBook(Long bookId) {
    final var book = bookRepository.findById(bookId).orElseThrow(() -> new NoSuchElementException(
        "Book is not found"
    ));
    book.sell();
    bookRepository.save(book);
  }
}
```

The code does not look like a list of commands anymore. The business case is *transparent* now. Besides, the `Book.sell` method might be reused in different application services. But the business rule remains the same.

The service that updates book info is gonna look familiar.

```java
@Service
public class BookUpdateServiceImpl implements BookUpdateService {
  private final BookRepository bookRepository;

  @Transactional
  @Override
  public void updateBookInfo(Long bookId, String name, String description) {
    final var book = bookRepository.findById(bookId).orElseThrow(() -> new NoSuchElementException(
        "Book is not found"
    ));
    book.changeInfo(name, description);
    bookRepository.save(book);
  }
}
```

## Introducing Domain Events

Now let's jump to the book archiving case. What if each book update published an event that would trigger book archiving? Well, Spring does have `ApplicationEventPublisher` bean that allows to publish events and subscribe to them by [`@EventListener`](https://www.baeldung.com/spring-events) usage.

```java
@Service
public class BookSaleServiceImpl implements BookSaleService {
  private final BookRepository bookRepository;
  private final ApplicationEventPublisher eventPublisher;

  @Transactional
  @Override
  public void sellBook(Long bookId) {
    final var book = bookRepository.findById(bookId).orElseThrow(() -> new NoSuchElementException(
        "Book is not found"
    ));
    // event publishing happens here
    eventPublisher.publishEvent(new BookUpdated(book));
    book.sell();
    bookRepository.save(book);
  }
}

@Component
public class BookUpdatedListener {
  @EventListener
  public void archiveBook(BookUpdated bookUpdated) {
    // do archiving
  }
}
```

Though it helps us to decouple the selling and the archiving process, it also forces us not to forget to publish `BookUpdated` on any book change.

We could provide `ApplicationEventPublisher` as a delegate to updating methods.

```java
@Entity
@Table
public class Book {
  ...

  public void sell(Supplier<? extends ApplicationEventPublisher> publisher) {
    publisher.get().publishEvent(new BookUpdated(this));
    final var bookSale = new BookSale();
    bookSale.setBook(this)
    bookSale.setDateSold(OffsetDateTime.now())
    bookSale.setPriceSold(price);
    bookSales.add(bookSale);
  }

  public void changeInfo(String name, String description, Supplier<? extends ApplicationEventPublisher> publisher) {
    publisher.get().publishEvent(new BookUpdated(this));
    this.name = name;
    this.description = description;
    lastDateUpdated = OffsetDateTime.now();
  }
```

That's better. But anyway, we have to inject this `ApplicationEventPublisher` instance to every service that somehow interacts with `Book`.

Is there any better solution? Sure. Embrace [`@DomainEvents`](https://www.baeldung.com/spring-data-ddd).

## @DomainEvents

```java
@Entity
@Table
public class Book {
  ...

  @Transient
  private final List<Object> domainEvents = new ArrayList<>();

  @DomainEvents
  public Collection<Object> domainEvents() {
    return Collections.unmodifiableList(this.domainEvents);
  }

  @AfterDomainEventPublication
  public void clearDomainEvents() {
    this.domainEvents.clear();
  }

  private void registerEvent(Object event) {
    this.domainEvents.add(event);
  }

  public void sell() {
    final var bookUpdated = new BookUpdated(this);
    final var bookSale = new BookSale();
    bookSale.setBook(this)
    bookSale.setDateSold(OffsetDateTime.now())
    bookSale.setPriceSold(price);
    bookSales.add(bookSale);
    registerEvent(bookUpdated);
  }

  public void changeInfo(String name, String description) {
    final var bookUpdated = new BookUpdated(this);
    this.name = name;
    this.description = description;
    lastDateUpdated = OffsetDateTime.now();
    registerEvent(bookUpdated);
  }
}
```

Every time the client calls `sell` or `changeInfo` method a `BookUpdated` event is added to `domainEvents` list. As you may guess, there is no direct publishing. So, how do events reach event listeners? When we call `Repository.save` method Spring collects the events by looking for `@DomainEvents` annotation. Then the cleanup is being processed (`@AfterDomainEventPublication`).

We could simplify it. Spring provides `AbstractAggregateRoot` class that already contains the required functionality. So, that's the less verbose option.

```java
@Entity
@Table
public class Book extends AbstractAggregateRoot<Book> {
  ...

  public void sell() {
    final var bookUpdated = new BookUpdated(this);
    final var bookSale = new BookSale();
    bookSale.setBook(this)
    bookSale.setDateSold(OffsetDateTime.now())
    bookSale.setPriceSold(price);
    bookSales.add(bookSale);
    registerEvent(bookUpdated);
  }

  public void changeInfo(String name, String description) {
    final var bookUpdated = new BookUpdated(this);
    this.name = name;
    this.description = description;
    lastDateUpdated = OffsetDateTime.now();
    registerEvent(bookUpdated);
  }
}
```

We forgot about email events. This could be tempting to declare `BookSaleEmailEvent` or `BookChangeInfoEmailEvent`. But that's would not be *domain-oriented*. You see, sending an email is just an implementation detail. There could be dozens of other options. Logging, putting a message to Kafka, triggering a job, etc. It's essential to focus on business use cases but not on functional behaviour.

So, the right way is to declare `BookSold` and `BookChangedInfo` events.

```java
@Entity
@Table
public class Book extends AbstractAggregateRoot<Book> {
  ...

  public void sell() {
    final var bookUpdated = new BookUpdated(this);
    final var bookSale = new BookSale();
    bookSale.setBook(this)
    bookSale.setDateSold(OffsetDateTime.now())
    bookSale.setPriceSold(price);
    bookSales.add(bookSale);
    registerEvent(bookUpdated);
    registerEvent(new BookSold(this));
  }

  public void changeInfo(String name, String description) {
    final var bookUpdated = new BookUpdated(this);
    this.name = name;
    this.description = description;
    lastDateUpdated = OffsetDateTime.now();
    registerEvent(bookUpdated);
    registerEvent(new BookChangedInfo(this));
  }
}
```

## Capturing events

The `@EventListener` annotation is a simple and convenient way to track Spring events. But there is a caveat. We don't need just to capture events. We want the listeners to be invoked in *particular moments* of the transaction lifecycle.

For example, the archiving should be done just before the transaction commit. If something goes wrong with the main request or the archiving itself, the whole transaction has to be rolled back.

On the contrary, an email should be sent right *after* transaction commit. If the request has not successfully proceeded, there is no need to notify anyone.

`@EventListener` annotation is not powerful enough to satisfy our needs. But no worries. `@TransactionalEventListener` to the rescue!

The difference is that the annotation provides the `phase` attribute. It declares the point of the transaction lifecycle when we listener has to be called. There are four possible values.

1. `BEFORE_COMMIT`
1. `AFTER_COMMIT` - the default one
1. `AFTER_ROLLBACK`
1. `AFTER_COMPLETION`

The first three options are self-explanatory. The `AFTER_COMPLETION` is the combination of `AFTER_ROLLBACK` and `AFTER_COMMIT`.

For example, that's how archiving books might be implemented.

```java
@Component
public class BookUpdatedListener {
  private final BookArchiveRepository bookArchiveRepository;

  @TransactionalEventListener(phase = BEFORE_COMMIT)
  public void archiveBook(BookUpdated bookUpdated) {
    BookArchive bookArchive = BookArchive.createNew(bookUpdated);
    bookArchiveRepository.save(bookArchive);
  }
}
``` 

> `BookArchive.createNew` just encapsulated the logic of creating the new `BookArchive` instance that has been described previously.

See? Piece of cake! Capturing `BookChangedInfo` and `BookSold` would be similar.

```java
@Component
public class BookChangedInfoListener {
  private final EmailService emailService;

  @TransactionalEventListener(phase = AFTER_COMMIT)
  public void notifyAuthorByEmail(BookChangedInfo bookChangedInfo) {
    String email = bookChangedInfo.getAuthorEmail();
    emailService.send(email, "Your book's info has been changed");
  }
}

@Component
public class BookSoldListener {
  private final EmailService emailService;

  @TransactionalEventListener(phase = AFTER_COMMIT)
  public void notifyAuthorIfNeeded(BookSold bookSold) {
    int totalSoldBooks = bookSold.getTotalSoldBooksCount();
    if (totalSoldBooks % 100 == 0) {
      String email = bookSold.getAuthorEmail();
      emailService.send(email, "Another 100 books of your have been sold!");
    }
  }
}
```

> There is an important detail about `@TransactionalEventListener`. Sometimes you need to invoke the commands in a new transaction on the `AFTER_COMMIT` phase. If so, make sure you put [`@Transactional(propagation = REQUIRES_NEW)`](https://www.baeldung.com/spring-transactional-propagation-isolation) as well. `REQUIRES_NEW` parameter is crucial. Because there might be a chance that previous transaction resources have not been cleaned up yet. So, we have to make sure that Spring starts a new one.

And now we can get rid of those decorators. So, here is the comparison between the first set-up and the final architecture.

#### The First Attempt

![The First Attempt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cqg9vru4ktf2u94wxyb9.png)

#### The Final Architecture

![The Final Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oczj5wgmyzrvy7q7nsn7.png)

The first approach puts all business logic inside the service layer, whilst domain classes act as simple data structures. This pattern is called [Transaction Script](https://martinfowler.com/eaaCatalog/transactionScript.html). If your system is small and non-complex, then it's fine to design the architecture across this pattern. But when it grows, it becomes hard to maintain.

> By the way, you probably don't need Spring Data and Hibernate, if you apply the Transaction Script pattern. Since all business rules are bound to services, Hibernate will bring overhead and not so many benefits. Instead, you could try to use [JDBI](https://jdbi.org/), [JOOQ](https://www.jooq.org/) or even plain JDBC.

The final architecture turns it upside down. Domain Entities encapsulate the business logic and the services act as thin wrappers (Rich Domain Model). No matter who interacts with the `Book` entity, the business rules remain the same. All the additional functionality is driven by Domain Events. That allows us to expand the system infinitely. Domain Events can trigger a variety of business operations. Putting a message to the queue, performing audit actions, notifying users, applying [CQRS](https://martinfowler.com/bliki/CQRS.html) pattern, etc.

## Conclusion
In my opinion, Hibernate combined with Spring Data is meant to be used with Domain Events. The benefits are worth it. I'm curious how do you apply persistence in your project? Do you prefer an Anemic or Rich Domain Model? Please, leave your comments down below. Thanks for reading!

## Resources
1. [Domain Event](https://martinfowler.com/eaaDev/DomainEvent.html) 
1. [Domain Driven Design](https://en.wikipedia.org/wiki/Domain-driven_design)
1. [Anemic Domain Model](https://martinfowler.com/bliki/AnemicDomainModel.html)
1. [Programmatic transactions](https://www.baeldung.com/spring-programmatic-transaction-management)
1. [Single-Responsibility Principle (SRP)](https://en.wikipedia.org/wiki/Single-responsibility_principle)
1. [Open-Closed Principle (OCP)](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle#:~:text=In%20object%2Doriented%20programming%2C%20the,without%20modifying%20its%20source%20code.)
1. [Decorator Pattern](https://en.wikipedia.org/wiki/Decorator_pattern)
1. [Spring Qualifier Annotation](https://www.baeldung.com/spring-qualifier-annotation)
1. [Hibernate First-level Cache](https://vladmihalcea.com/jpa-hibernate-first-level-cache/)
1. [Spring Event Listener](https://www.baeldung.com/spring-events)
1. [Spring Domain Events](https://www.baeldung.com/spring-data-ddd)
1. [Spring Transactional Propagation and Isolation](https://www.baeldung.com/spring-transactional-propagation-isolation)
1. [Transaction Script Pattern](https://martinfowler.com/eaaCatalog/transactionScript.html)
1. [JDBI](https://jdbi.org/)
1. [JOOQ](https://www.jooq.org/)
1. [CQRS Pattern](https://martinfowler.com/bliki/CQRS.html)