Introduction

In modern software development, the alignment between business requirements and code implementation remains a critical challenge. This article examines the application of domain-driven design principles in Java, specifically focusing on how value objects can be employed to maintain business requirements in close proximity to their implementation.

Problem Statement

Consider a scenario where a software development team is tasked with implementing a library management system. The business domain contains well-established concepts such as ISBN numbers, book titles, and author names, each with specific validation rules and constraints. However, traditional object-oriented approaches often fail to capture these domain-specific requirements directly within the code structure.

The conventional approach involves creating a simple data structure to represent a book entity:

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);
    }
}

While this implementation appears straightforward, it presents several critical issues. The primary concern is the lack of data validation, allowing the creation of invalid book instances through the constructor with null or malformed parameters. This represents a fundamental flaw in the design that can lead to runtime errors and data integrity issues.

To address the null parameter vulnerability, a preliminary solution involves implementing null checks within the record constructor:

import java.util.Objects;

public record Book(String ISBN, String name, String description, String authorName, String bookCover) {
    public Book {
        Objects.requireNonNull(ISBN);
        Objects.requireNonNull(name);
        Objects.requireNonNull(description);
        Objects.requireNonNull(authorName);
        Objects.requireNonNull(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));
    }
}

The implementation of null validation provides a basic level of protection against invalid constructor arguments. However, this approach addresses only a subset of potential validation issues. The system remains vulnerable to semantically incorrect but non-null parameters:

import java.util.Objects;

public record Book(String ISBN, String name, String description, String authorName, String bookCover) {
    public Book {
        Objects.requireNonNull(ISBN);
        Objects.requireNonNull(name);
        Objects.requireNonNull(description);
        Objects.requireNonNull(authorName);
        Objects.requireNonNull(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));
    }
}

The example demonstrates a critical flaw: while the string "coolISBN" is not null, it does not constitute a valid ISBN according to international standards. This highlights the necessity for domain-specific validation beyond simple null checks.

Domain Requirements Analysis

The International Standard Book Number (ISBN) system defines specific formatting and validation rules that must be enforced at the application level. According to the business requirements:

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.

To implement proper ISBN validation, a regular expression pattern matching approach is required. The following implementation incorporates domain-specific validation rules:

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.requireNonNull(ISBN);
        if (!isValidISBN(ISBN)) {
            throw new IllegalArgumentException("ISBN is not valid");
        }

        Objects.requireNonNull(name);
        Objects.requireNonNull(description);
        Objects.requireNonNull(authorName);
        Objects.requireNonNull(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"));
    }
}

While ISBN validation addresses one aspect of the domain requirements, additional fields require similar attention. Each string-based field possesses distinct validation rules that must be enforced to maintain data integrity. The business requirements specify the following constraints:

- 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.

A straightforward approach involves incorporating all validation logic within the record constructor. This centralizes validation concerns but leads to increased complexity:

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.requireNonNull(ISBN);
        if (!isValidISBN(ISBN)) {
            throw new IllegalArgumentException("ISBN is not valid");
        }

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

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

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

        Objects.requireNonNull(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 cannotCreateBookWithWrongDescription() {
        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"));
    }
}

While this implementation ensures data validation, it introduces several architectural concerns. The extensive use of generic String types throughout the system creates a lack of semantic meaning and type safety. When business logic interacts with these fields, developers must work with primitive strings rather than domain-specific types, as demonstrated in the following service class:

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();
    }
}

This implementation demonstrates the cognitive burden imposed by primitive string manipulation. The extraction logic is complex, error-prone, and divorced from the domain concept it represents. While refactoring could relocate this logic to utility methods or separate classes, such approaches fundamentally separate domain concepts from their inherent business rules and characteristics. The use of generic String types eliminates semantic context, creating maintenance challenges and reducing code expressiveness.

Value Objects as Domain Abstractions

Value objects provide an architectural solution that addresses the limitations of primitive types while encapsulating domain-specific behavior. A value object represents an immutable type distinguished solely by its constituent properties, offering several architectural advantages:

Immutability: Once instantiated, the object's state remains constant, ensuring data consistency across application boundaries.

Encapsulated Validation: Domain-specific validation rules are colocated with the concept they govern, improving maintainability and reducing coupling.

Semantic Clarity: Type-safe abstractions provide explicit domain meaning, enhancing code readability and reducing ambiguity.

Behavioral Enrichment: Value objects can expose domain-specific operations, eliminating the need for external utility functions.

The following implementation demonstrates the transformation of the ISBN field into a proper value object:

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.requireNonNull(value);
        if (!isValidISBN(value)) {
            throw new IllegalArgumentException("ISBN is not valid");
        }
    }

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

Subsequently, the Book record is modified to utilize the ISBN value object rather than a primitive string:

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.requireNonNull(isbn);

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

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

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

        Objects.requireNonNull(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;
        }
    }

}

This refactoring achieves several architectural improvements: the cognitive complexity of the Book class is significantly reduced, maintainability is enhanced through clear separation of concerns, and the responsibility for ISBN validation is appropriately delegated to the domain concept itself.

The implementation requires a minor adjustment in object instantiation, as the ISBN value object must be explicitly created:

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"));
    }
}

The previously demonstrated service method that extracted country codes from ISBN strings can be significantly improved through value object encapsulation. By relocating the extraction logic to the ISBN class itself, we achieve better cohesion and eliminate external dependencies on internal string manipulation:

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.requireNonNull(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);
    }

}

This approach demonstrates significant architectural improvement through knowledge encapsulation. The domain-specific logic is transferred from external services to the appropriate value object, enhancing readability while providing a clean interface for accessing derived information.

The refactored service method exhibits improved clarity and reduced complexity:

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();
    }
}

The transformation from primitive strings to semantic types enables the addition of domain-specific methods such as EAN prefix extraction, publisher code identification, and control digit validation. The extensibility of this approach allows for future enhancements based on evolving business requirements.

Applying the same principles to all domain concepts results in a fully encapsulated Book entity:

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);
    }
}

The implementation of remaining value objects follows the established pattern, with each encapsulating its respective validation logic and domain behavior:

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;
        }
    }
}

The final implementation demonstrates a comprehensive approach to domain modeling where business requirements are directly encoded within the type system:

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

This article has demonstrated the evolutionary progression from primitive string-based data structures to sophisticated domain-driven value objects. The initial approach, while functional, suffered from several architectural deficiencies: lack of semantic meaning, centralized validation logic, and poor encapsulation of domain behavior.

Through the systematic application of value object patterns, we achieved significant improvements in code quality and maintainability. Each domain concept now encapsulates its validation rules and behavioral operations, creating a self-contained and semantically rich model. The resulting architecture ensures that business requirements are directly embedded within the code structure, reducing the likelihood of inconsistencies between domain specifications and implementation.

The value object approach provides several key benefits: enhanced type safety through elimination of primitive obsession, improved code readability through semantic typing, better maintainability through encapsulated validation logic, and increased extensibility through domain-specific method exposure. Most importantly, this pattern ensures that once an object is successfully instantiated, it is guaranteed to satisfy all business requirements, providing strong compile-time guarantees about data integrity.


This article was developed using generative AI under human supervision and guidance. The ideas, structure, and experiences shared reflect real-world development challenges, enhanced through AI-assisted writing to ensure clarity and comprehensiveness.