Keep requirements close to your code
Tags: java, ddd, domain
Published: 2023-11-29 21:42:59

## Preface

You are a software developer who has joined a new project about a library where you have been given a set of requirements that the project must comply. This project is a Java application that you need to code using the latest super Java technologies.

## Diving in

We start by reading the requirements and trying to understand the concepts, functions, data...etc.

Well, we are no strangers to concepts like book cover, ISBN or book name, but when you code, these concepts are not present from scratch. So, like we would do when we start a project, we create a new project, we create packages, we create classes... but this time we would like to focus on domain driven design.

While coding in Java, we will create a record called ´Book´ where all the attributes will be placed, so that we can instantiate books easily.

```java
public record Book(String ISBN, String name, String description, String authorName, String bookCover) {
}

class BookTest {

    @Test
    void createBook() {
        Book elQuixote = new Book("978-84-08-06105-2", "Don Quixote de la Mancha",
                "Don Quixote is a Spanish epic novel by Miguel de Cervantes. It was originally published in two parts, in 1605 and 1615.",
                "Miguel de Cervantes",
                "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Title_page_first_edition_Don_Quijote.jpg/800px-Title_page_first_edition_Don_Quijote.jpg");

        assertNotNull(elQuixote);
    }
}
```

This approach may seem simple and good. However, when we start thinking... Can we create a Book with incorrect parameters? It is definitely possible using only the `new Book(null, null, null, null, null)` constructor. Oh, the million dollar mistake. Easy solution, but not pretty (java stuff):

```java
import java.util.Objects;

public record Book(String ISBN, String name, String description, String authorName, String bookCover) {
    public Book {
        Objects.requireNotNull(ISBN);
        Objects.requireNotNull(name);
        Objects.requireNotNull(description);
        Objects.requireNotNull(authorName);
        Objects.requireNotNull(bookCover);
    }
}

class BookTest {

    @Test
    void createBook() {
        final Book elQuixote = new Book("978-84-08-06105-2", "Don Quixote de la Mancha",
                "Don Quixote is a Spanish epic novel by Miguel de Cervantes. It was originally published in two parts, in 1605 and 1615.",
                "Miguel de Cervantes",
                "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Title_page_first_edition_Don_Quijote.jpg/800px-Title_page_first_edition_Don_Quijote.jpg");

        assertNotNull(elQuixote);
    }

    @Test
    void cannotCreateBookWithNullParameters() {
        assertThrows(NullPointerException.class, () -> new Book(null, null, null, null, null));
    }
}
```

With those `requireNotNull` we are safe because no null parameter can be injected into the constructor... Wait, are we completely safe?

```java
import java.util.Objects;

public record Book(String ISBN, String name, String description, String authorName, String bookCover) {
    public Book {
        Objects.requireNotNull(ISBN);
        Objects.requireNotNull(name);
        Objects.requireNotNull(description);
        Objects.requireNotNull(authorName);
        Objects.requireNotNull(bookCover);
    }
}

class BookTest {

    @Test
    void createBook() {
        final Book elQuixote = new Book("978-84-08-06105-2", "Don Quixote de la Mancha",
                "Don Quixote is a Spanish epic novel by Miguel de Cervantes. It was originally published in two parts, in 1605 and 1615.",
                "Miguel de Cervantes",
                "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Title_page_first_edition_Don_Quijote.jpg/800px-Title_page_first_edition_Don_Quijote.jpg");

        assertNotNull(elQuixote);

        // WATCH OUT
        final Book otherQuixoteBook = new Book("coolISBN", "Don Quixote de la Mancha",
                "Don Quixote is a Spanish epic novel by Miguel de Cervantes. It was originally published in two parts, in 1605 and 1615.",
                "Miguel de Cervantes",
                "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Title_page_first_edition_Don_Quijote.jpg/800px-Title_page_first_edition_Don_Quijote.jpg");

        assertNotNull(otherQuixoteBook);
    }

    @Test
    void cannotCreateBookWithNullParameters() {
        assertThrows(NullPointerException.class, () -> new Book(null, null, null, null, null));
    }
}
```

Hold on... This `coolISBN` in `otherQuixoteBook` does not look like a valid ISBN for a book. We may need to add some sort of `validation` mecahism for the ISBN parameter. We might be safe from null parameters, but not from non-valid ISBN parameters.

According to the domain of this problem, an **ISBN** is an International Standard Book Number. Now, in our requirements there is a description about an ISBN:

```
An ISBN consists of 13 digits (until December 2006 it had 10 digits), the first 3 digits are an EAN prefix and it is always 978 or 979. 979, then there are 2 digits identifying the country (in the case of Spain 84), then we have 3 digits identifying the publisher (or the self-publisher), the next 3 digits identify the publisher (or the self-publisher), the next 3 digits identify the book, and the last digit (the letter X can also appear, which is equivalent to 10) serves as a control 10) serves as a check to confirm that the string is correct.
```

Okay, we _obviously_ need a **regex** for this ISBN thing.

```java
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Objects;
import java.util.regex.Pattern;

public record Book(String ISBN, String name, String description, String authorName, String bookCover) {
    private static final String ISBN_PATTERN = "^(ISBN(-13)?:\\s?)?(\\d{3}-?\\d{1,5}-?\\d{1,7}-?\\d{1,7}-?\\d{1,7}|\\d{10}|\\d{13})$";

    public Book {
        Objects.requireNotNull(ISBN);
        if (!isValidISBN(ISBN)) {
            throw new IllegalArgumentException("ISBN is not valid");
        }

        Objects.requireNotNull(name);
        Objects.requireNotNull(description);
        Objects.requireNotNull(authorName);
        Objects.requireNotNull(bookCover);
    }


    private static boolean isValidISBN(String isbn) {
        return Pattern.matches(ISBN_PATTERN, isbn);
    }
}

class BookTest {

    @Test
    void createBook() {
        final Book elQuixote = new Book("978-84-08-06105-2", "Don Quixote de la Mancha",
                "Don Quixote is a Spanish epic novel by Miguel de Cervantes. It was originally published in two parts, in 1605 and 1615.",
                "Miguel de Cervantes",
                "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Title_page_first_edition_Don_Quijote.jpg/800px-Title_page_first_edition_Don_Quijote.jpg");

        assertNotNull(elQuixote);
    }

    @Test
    void cannotCreateBookWithNullParameters() {
        assertThrows(NullPointerException.class, () -> new Book(null, null, null, null, null));
    }

    @Test
    void cannotCreateBookWithWrongISBN() {
        assertThrows(IllegalArgumentException.class, () -> new Book("coolISBN", "Don Quixote de la Mancha",
                "Don Quixote is a Spanish epic novel by Miguel de Cervantes. It was originally published in two parts, in 1605 and 1615.",
                "Miguel de Cervantes",
                "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Title_page_first_edition_Don_Quijote.jpg/800px-Title_page_first_edition_Don_Quijote.jpg"));
    }
}
```

Well, NOW we are safe... but not quite. What about the cover of the book? Or the name, description or author's name? Or any other new field we would like to add? These are strings that have a specific format. According to the requirements we have the following information about these fields:

```
- The name of the book must not be empty and must have a maximum length of 80 characters.
- The book description must not be empty and must have a maximum length of 200 characters.
- The author's name must not be empty.
- The book cover is a URL of an image.
```

The first option that comes to my mind is adding these validations in the record construction.

```java
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Objects;
import java.util.regex.Pattern;

public record Book(String ISBN, String name, String description, String authorName, String bookCover) {
    private static final String ISBN_PATTERN = "^(ISBN(-13)?:\\s?)?(\\d{3}-?\\d{1,5}-?\\d{1,7}-?\\d{1,7}-?\\d{1,7}|\\d{10}|\\d{13})$";
    private static final Pattern IMAGE_EXTENSION_PATTERN = Pattern.compile("\\.(jpg|png)$", Pattern.CASE_INSENSITIVE);

    public Book {
        Objects.requireNotNull(ISBN);
        if (!isValidISBN(ISBN)) {
            throw new IllegalArgumentException("ISBN is not valid");
        }

        Objects.requireNotNull(name);
        if (!nameIsValid()) {
            throw new IllegalArgumentException("name is not valid");
        }

        Objects.requireNotNull(description);
        if (!descriptionIsValid(description)) {
            throw new IllegalArgumentException("description is not valid");
        }

        Objects.requireNotNull(authorName);
        if (!authorNameIsValid(authorName)) {
            throw new IllegalArgumentException("authorName is not valid");
        }

        Objects.requireNotNull(bookCover);
        if (!bookCoverIsValid(bookCover)) {
            throw new IllegalArgumentException("bookCover is not valid");
        }
    }

    private static boolean isValidISBN(String isbn) {
        return Pattern.matches(ISBN_PATTERN, isbn);
    }

    private static boolean nameIsValid(String name) {
        return name.length > 0 && name.length <= 80 && !name.trim().equals(" ");
    }

    private static boolean descriptionIsValid(String description) {
        return description.length > 0 && description.length <= 200 && !description.trim().equals(" ");
    }

    private static boolean authorNameIsValid(String authorName) {
        return authorName.length > 0 && !authorName.trim().equals(" ");
    }

    private static boolean bookCoverIsValid(String imageUrl) {
        try {
            URL url = new URL(imageUrl);
            String path = url.getPath();
            return IMAGE_EXTENSION_PATTERN.matcher(path).find();
        } catch (MalformedURLException e) {
            return false;
        }
    }

}

class BookTest {

    @Test
    void createBook() {
        final Book elQuixote = new Book("978-84-08-06105-2", "Don Quixote de la Mancha",
                "Don Quixote is a Spanish epic novel by Miguel de Cervantes. It was originally published in two parts, in 1605 and 1615.",
                "Miguel de Cervantes",
                "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Title_page_first_edition_Don_Quijote.jpg/800px-Title_page_first_edition_Don_Quijote.jpg");

        assertNotNull(elQuixote);
    }

    @Test
    void cannotCreateBookWithNullParameters() {
        assertThrows(NullPointerException.class, () -> new Book(null, null, null, null, null));
    }

    @Test
    void cannotCreateBookWithWrongISBN() {
        assertThrows(IllegalArgumentException.class, () -> new Book("coolISBN", "Don Quixote de la Mancha",
                "Don Quixote is a Spanish epic novel by Miguel de Cervantes. It was originally published in two parts, in 1605 and 1615.",
                "Miguel de Cervantes",
                "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Title_page_first_edition_Don_Quijote.jpg/800px-Title_page_first_edition_Don_Quijote.jpg"));
    }

    @Test
    void cannotCreateBookWithWrongName() {
        assertThrows(IllegalArgumentException.class, () -> new Book("978-84-08-06105-2", "",
                "Don Quixote is a Spanish epic novel by Miguel de Cervantes. It was originally published in two parts, in 1605 and 1615.",
                "Miguel de Cervantes",
                "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Title_page_first_edition_Don_Quijote.jpg/800px-Title_page_first_edition_Don_Quijote.jpg"));
    }

    @Test
    void cannotCreateBookWithWrongDescrpition() {
        assertThrows(IllegalArgumentException.class, () -> new Book("978-84-08-06105-2", "Don Quixote de la Mancha",
                "",
                "Miguel de Cervantes",
                "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Title_page_first_edition_Don_Quijote.jpg/800px-Title_page_first_edition_Don_Quijote.jpg"));
    }

    @Test
    void cannotCreateBookWithWrongAuthorName() {
        assertThrows(IllegalArgumentException.class, () -> new Book("978-84-08-06105-2", "Don Quixote de la Mancha",
                "Don Quixote is a Spanish epic novel by Miguel de Cervantes. It was originally published in two parts, in 1605 and 1615.",
                "",
                "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Title_page_first_edition_Don_Quijote.jpg/800px-Title_page_first_edition_Don_Quijote.jpg"));
    }

    @Test
    void cannotCreateBookWithWrongBookCover() {
        assertThrows(IllegalArgumentException.class, () -> new Book("978-84-08-06105-2", "Don Quixote de la Mancha",
                "Don Quixote is a Spanish epic novel by Miguel de Cervantes. It was originally published in two parts, in 1605 and 1615.",
                "Miguel de Cervantes",
                "picture"));
    }
}
```

This looks like a large class, but we are safe for now. However, the more I read the `Book` constructor the more I think we could add more information because we are always using `String` all over the place for the fields. If we had to interact with `Book` fields in a hypothetical `BookService` we would always interact with strings.

```java
class MyFantasticBookService {

    private final List<Book> collectionOfBooks = populateCollectionOfBooksFromPersistence();

    // Remember, 84 is the country code for Spain
    public List<Book> getAllBooksWithCountryCodeFromSpain() {
        return collectionOfBooks.stream().filter(book -> {
            final String countryCode = book.isbn().replaceAll("-", "").replace("ISBN-13:", "").replace("ISBN:", "").trim().substring(3, 5);
            return countryCode.equals("84");
        }).toList();
    }
}
```

Wow, this looks tricky, doesn't it? We are using plain `String` to extract a specific piece of information about a field, in this case the ISBN. We could start refactoring and extracting this chain of alterations of the string to methods in this service or even in a separated class with static or methods in the `Book` class... but all these options make us separate the concept of domain in the code from its restrictions or characteristics according to the requirements. ISBN as a plain `String` has **no context around it** and that is something we want to avoid.

## Value objects to the rescue

Using value objects for this matter is an interesting choice. A value objects is an immutable type that is distinguishable only by the state of its properties. What if the ISBN was a value object? We would benefit from:

- Once it is created, it will never change its value. However, with String we have the same benefit.
- The validation methods are exactly where they are supposed to be. Close to the concept definition, as a part of it, actually.
- It is semantic. Whenever we see `ISBN` we automatically know what are we talking about.
- We can provide semantic methods to interact with the value (remember country code?).

Let's get on it. First, we create the record `ISBN` but moving the validation stuff from `Book` to the `ISBN` construction.

```java
public record ISBN(String value) {

    private static final String ISBN_PATTERN = "^(ISBN(-13)?:\\s?)?(\\d{3}-?\\d{1,5}-?\\d{1,7}-?\\d{1,7}-?\\d{1,7}|\\d{10}|\\d{13})$";

    public ISBN {
        Objects.requireNotNull(value);
        if (!isValidISBN(value)) {
            throw new IllegalArgumentException("ISBN is not valid");
        }
    }

    private static boolean isValidISBN(String isbn) {
        return Pattern.matches(ISBN_PATTERN, isbn);
    }
}
```

Then, we change the `ISBN` parameter type from the `Book` record.

```java
public record Book(ISBN isbn, String name, String description, String authorName, String bookCover) {
    private static final Pattern IMAGE_EXTENSION_PATTERN = Pattern.compile("\\.(jpg|png)$", Pattern.CASE_INSENSITIVE);

    Book {
        Objects.requireNotNull(isbn);

        Objects.requireNotNull(name);
        if (!nameIsValid()) {
            throw new IllegalArgumentException("name is not valid");
        }

        Objects.requireNotNull(description);
        if (!descriptionIsValid(description)) {
            throw new IllegalArgumentException("description is not valid");
        }

        Objects.requireNotNull(authorName);
        if (!authorNameIsValid(authorName)) {
            throw new IllegalArgumentException("authorName is not valid");
        }

        Objects.requireNotNull(bookCover);
        if (!bookCoverIsValid(bookCover)) {
            throw new IllegalArgumentException("bookCover is not valid");
        }
    }

    private static boolean nameIsValid(String name) {
        return name.length > 0 && name.length <= 80 && !name.trim().equals(" ");
    }

    private static boolean descriptionIsValid(String description) {
        return description.length > 0 && description.length <= 200 && !description.trim().equals(" ");
    }

    private static boolean authorNameIsValid(String authorName) {
        return authorName.length > 0 && !authorName.trim().equals(" ");
    }

    private static boolean bookCoverIsValid(String imageUrl) {
        try {
            URL url = new URL(imageUrl);
            String path = url.getPath();
            return IMAGE_EXTENSION_PATTERN.matcher(path).find();
        } catch (MalformedURLException e) {
            return false;
        }
    }

}
```

Do you see the difference? We have reduced the cognitive complexity from the `Book` class and improved maintainability by delegating the responsibility of representing the ISBN to the corresponding record `IBSN`, forgetting completely about ensuring it, in fact, is a valid ISBN.

It comes with a cheap price to pay when are creating a book though, the ISBN needs to be created before the Book:

```java
class BookTest {

    @Test
    void createBook() {
        final Book elQuixote = new Book(new ISBN("978-84-08-06105-2"), "Don Quixote de la Mancha",
                "Don Quixote is a Spanish epic novel by Miguel de Cervantes. It was originally published in two parts, in 1605 and 1615.",
                "Miguel de Cervantes",
                "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Title_page_first_edition_Don_Quijote.jpg/800px-Title_page_first_edition_Don_Quijote.jpg");

        assertNotNull(elQuixote);
    }

    @Test
    void cannotCreateBookWithWrongISBN() {
        assertThrows(IllegalArgumentException.class, () -> new Book(new ISBN("coolISBN"), "Don Quixote de la Mancha",
                "Don Quixote is a Spanish epic novel by Miguel de Cervantes. It was originally published in two parts, in 1605 and 1615.",
                "Miguel de Cervantes",
                "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Title_page_first_edition_Don_Quijote.jpg/800px-Title_page_first_edition_Don_Quijote.jpg"));
    }
}
```

Now, let's remember the method from the previous hypothetical service `MyFantasticBookService`. The `getAllBooksWithCountryCodeFromSpain
()` method took the plain `String` value and transformed it until the country code from the ISBN was extracted. What if we moved the extraction to a class method of `ISBN`?. It seems reasonable because only the `ISBN` class has the value and knows "how to deal with it".

```java
public record ISBN(String value) {

    private static final String ISBN_PATTERN = "^(ISBN(-13)?:\\s?)?(\\d{3}-?\\d{1,5}-?\\d{1,7}-?\\d{1,7}-?\\d{1,7}|\\d{10}|\\d{13})$";

    public ISBN {
        Objects.requireNotNull(value);
        if (!isValidISBN(value)) {
            throw new IllegalArgumentException("ISBN is not valid");
        }
    }

    private static boolean isValidISBN(String isbn) {
        return Pattern.matches(ISBN_PATTERN, isbn);
    }

    public String countryCode() {
        return value.replaceAll("-", "").replace("ISBN-13:", "").replace("ISBN:", "").trim().substring(3, 5);
    }

}
```

Wow, that's an improvement! We have transferred the knowledge from `Book` to `ISBN` improving readability and allowing `Book` to access to the same piece of information by using the `countryCode` method.

How would `MyFantasticBookService` look like?

```java
class MyFantasticBookService {

    private final List<Book> collectionOfBooks = populateCollectionOfBooksFromPersistence();

    public List<Book> getAllBooksWithCountryCodeFromSpain() {
        // That 84 looks like a code smell indeed
        return collectionOfBooks.stream().filter(book -> book.isbn().countryCode().equals("84")).toList();
    }
}
```

We have transformed a plain `String` with no context to a semantic type `ISBN` where we could add any method related to the `ISBN`, for example getting the EAN prefix, the publisher code or the control digit. It will depend on the domain needs.

We won't stop with this type only, will we? We want to do the same thing with the other fields so `Book` ends up looking like this:

```java
import java.util.Objects;

public record Book(ISBN isbn, BookName name, BookDescription description, AuthorName authorName, BookCover bookCover) {

    public Book {
        Objects.requireNonNull(isbn);
        Objects.requireNonNull(name);
        Objects.requireNonNull(description);
        Objects.requireNonNull(authorName);
        Objects.requireNonNull(bookCover);
    }
}
```

For that we would follow the same approach as we did with `ISBN`.

```java
public record BookName(String value) {
    public BookName {
        Objects.requireNonNull(value);
        if (!nameIsValid(value)) {
            throw new IllegalArgumentException("Name is not valid");
        }
    }

    private static boolean nameIsValid(String name) {
        return name.length() > 0 && name.length() <= 80 && !name.trim().equals("");
    }
}

public record Description(String value) {
    public Description {
        Objects.requireNonNull(value);
        if (!descriptionIsValid(value)) {
            throw new IllegalArgumentException("Description is not valid");
        }
    }

    private static boolean descriptionIsValid(String description) {
        return description.length() > 0 && description.length() <= 200 && !description.trim().equals("");
    }
}

public record AuthorName(String value) {
    public AuthorName {
        Objects.requireNonNull(value);
        if (!authorNameIsValid(value)) {
            throw new IllegalArgumentException("Author name is not valid");
        }
    }

    private static boolean authorNameIsValid(String authorName) {
        return authorName.length() > 0 && !authorName.trim().equals("");
    }
}

public record BookCover(String value) {
    private static final Pattern IMAGE_EXTENSION_PATTERN = Pattern.compile("\\.(jpg|png)$", Pattern.CASE_INSENSITIVE);

    public BookCover {
        Objects.requireNonNull(value);
        if (!bookCoverIsValid(value)) {
            throw new IllegalArgumentException("Book cover is not valid");
        }
    }

    private static boolean bookCoverIsValid(String imageUrl) {
        try {
            URL url = new URL(imageUrl);
            String path = url.getPath();
            return IMAGE_EXTENSION_PATTERN.matcher(path).find();
        } catch (MalformedURLException e) {
            return false;
        }
    }
}
```

And then, finally, we have a proper way to create a `Book` with requirements close to the code.

```java
class BookTest {

    @Test
    void createBook() {
        final Book elQuixote = new Book(new ISBN("978-84-08-06105-2"),
                new BookName("Don Quixote de la Mancha"),
                new BookDescription(
                        "Don Quixote is a Spanish epic novel by Miguel de Cervantes. It was originally published in two parts,in 1605 and 1615."),
                new AuthorName("Miguel de Cervantes"),
                new BookCover(
                        "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Title_page_first_edition_Don_Quijote. jpg/800px-Title_page_first_edition_Don_Quijote.jpg"));

        assertNotNull(elQuixote);
    }
}
```

## Conclusion

We started by using plain strings as values in `Book` with no context at all, with validation in the `Book` constructor and not providing any kind of method to interact with values. We transitioned from that into value objects with responsibility for each concrete value, improving readability and making the `Book` life easier. Also, we provide better developer experience by ensuring once the Book is created, it is meeting all **requirements**.