Send your request Join Sii

With every project we encounter, we are presented with plenty of choices: technologies, tools, languages, as well as software design approaches. Each decision carries a lot of weight, as it later determines how feasible it is to grow applications and maintain them. Patterns and designs exist to assist with their size and complexity.

One major design approach is DDD, known as domain-driven design. The main idea behind DDD is to focus on modeling applications in a way that aligns with input from a specialist in that particular area.

Benefits of using a Domain Driven Design:

  • Patterns helping to solve complex and difficult problems.
  • Clear, readable, and testable code that is well-divided.
  • Brings together technical and business aspects of the design process.

From a developer’s point of view, it also greatly helps to understand the problem that the software is supposed to solve, rather than interpreting requirements on our own. The developer’s understanding might differ significantly from what is actually needed. After all, DDD is about solving problems, not just writing code.

When not to use DDD

DDD is not the answer to everything, and there are cases where using it might not provide enough benefits to outweigh the additional work that comes with employing this approach.

The signs that it’s probably a good idea to skip using domain-driven design:

  • If the application has simple CRUD (Create, Read, Update, Delete) operations.
  • If there are only a few business processes.
  • If there are no plans to increase the complexity.

Complexity is a key factor to evaluate our need for DDD. Quite often, it is easy to underestimate it and not judge accordingly. From my experience, there are times when unloading a full domain-driven design approach might be demanding and not provide significant benefits. In such cases, even if a project is run without using proper DDD, there are still certain techniques that can be useful and have their place. In a later section, I will share some of those techniques that are useful regardless!

Architecture

We can divide a typical enterprise application into the following layers:

  1. User interface: this layer is responsible for communication and presenting information to the user.
  2. Application Layer: provides communication with the Domain Layer and coordinates the data flow between multiple domain services. It is important to note that the application layer should not hold any business logic.
  3. Domain Layer: this layer contains the business operations logic, which should be handled by appropriate domain services. Entity state and behavior are managed here. Communication with external services, as well as persistence, is forwarded to the infrastructure layer.
  4. Infrastructure Layer: its main responsibility is to provide infrastructure support for other layers.

The key layers of DDD

The Application Layer

Ensures communication between domain services, as they cannot communicate directly with each other. In this layer, data transfer objects (DTOs) are defined to facilitate the transfer of data to the user interface. We can also perform basic validations, not business-related, before allowing them into lower layers — more about them later. During modeling, our use cases are often divided into command classes, which also belong in the application layer.

The Domain Layer

It is the heart of the application, but at the same time, it should be well isolated from other layers. It cannot rely on frameworks used elsewhere. This is where all the business logic is implemented. The domain layer consists of entities, interfaces for repositories, domain services, and, if applicable, events. Also, here we are going to be defining Entities as well as VO (Value Objects).

It is important to remember that an entity is a class that contains some properties with a global identity, which persists throughout its lifespan. Although the state of properties may change, the identity remains constant. On the other hand, Value Objects are immutable and are mostly used to isolate complex logic from entities, thereby reducing their complexity.

As we briefly discussed Value Objects (VOs) and entities, which are crucial in domain-driven design (DDD), let’s delve deeper into their understanding.

Entities and Value Objects

In our example, we will use a Person as an entity.

public class Person {
    private int id;
    private String name;
    private Address address;
}

And its value object is an Address.

public class Address {
  private String street;
  private int number;
  private String city;
}

Address has no identity; it exists in the way it has been created. Changing any of the attributes would make it a new address. Replacing existing values on existing objects would result in a violation of Value Object rules. We could think of an address in the same way we think of other primitive data types. Depriving our objects of their identity simplifies our design and helps with performance.

Getting back to our example, multiple people could live at the same address. They will share the same instance of an address. In the event that one of them changes their location, we would assign a new instance to that person instead of modifying an existing object.

Simple use case with two instances of entity Person sharing the same Value Object Address
Fig.1 Simple use case with two instances of entity Person sharing the same Value Object Address
Database representation with use of entity and VO principles
Fig. 2 Database representation with use of entity and VO principles
Example that volatiles rules of Value objects by assigning identity to address:
Fig. 3 Example that volatiles rules of Value objects by assigning identity to address

Validations

Validations can be performed on different layers, depending on their scope. We can categorize them into two types: superficial validations, which areperformed at a basic level checking type of input, and domain validations, which ensure validity at a business level.

It is worth mentioning that validations might be generic, such as ensuring phone numbers consist of only digits or enforcing passwords to be a string with more than 8 characters. On the other hand, there are specific validations where we might require that the phone number is unique or that the new password is different from the previous one.

Generic rules should be checked directly at the entity level, while specific rules should be validated at the time of creation. It is considered a good practice to prevent objects from being in an invalid state by implementing proactive validations.

Testing

A frequent situation for developers and the creation of software, in general, is lack of understanding of what to test, which is often related to how the software is designed. Fortunately, one of the benefits of DDD is its ease of testability, which can be further extended by using the Test-Driven Development (TDD) approach.

In fact, domain-driven design is very well-suited for test-first development because behavior, together with state, is included in domain, allowing for isolated testing. This approach not only provides us with early feedback before any user has a chance to touch our software but also enhances our understating of the use cases.

To illustrate this, let’s consider the scenario of running a library. Our desired behavior is to enable our customers to borrow a certain number of books at once. If we focus on the interaction of our domains, our test might look like this:

List<Book> books = List.of(“book1”, “book2”, “book3”);
Order order = Order.createOrder(user, books);
assertTrue(order.canOrder());

The assumption for that test is that one order can have a maximum of 3 books in which case it would pass. All tests we create, regardless of methodology, should be maintained and executed frequently. It is recommended to integrate running tests into the pipeline to ensure high quality of code.

Events

Events are commonly used in applications developed using the DDD approach to explicitly implement side effects. Those often improve the scalability of our software and help with handling operations. Events can be divided into Domain and Integration Events.

Domain events occur within a single boundary. When an event is triggered and published, it is processed by consumers that are bound to the same domain. No message brokers are utilized, and often can be wrapped within the same database transaction. Events are represented as objects within our domain model.

Domain event
Fig. 4 Domain event

Integration events, on the other hand, are primarily used for integration with other service boundaries. In this scenario, a message broker is employed, to which other boundaries can subscribe. Each consumer processes the events on their own in isolation, without impacting other parties. With that in mind, it is a good practice to ensure that integration events are properly validated before being sent. If we start sending non-persisted data, we can quickly encounter a lot of inconsistent data across multiple boundaries or microservices.

Integration event
Fig. 5 Integration event

Non-DDD projects

Even if we are not using DDD there are still options and hope! Value Objects is my favorite aspect of DDD that can be utilized in projects that happen not to run DDD or those that do not have enough justification for it.

We have already discussed what value objects are, and since we are familiar with them, the question is how we can use them in other environments. First of all, since value objects do not possess identity, those should be interchangeable. Without identity and mutability, they can be replaced with a new instance (as per our examples above).

When our value objects become larger in size, it is good to provide builders that facilitate their clean creation. My preferred way is to utilize static factory methods, but feel free to use constructors or builders.

Since these objects are immutable, we do not need to worry about side effects or thread safety as well. Here comes, in my opinion, the best part about them — the business logic we incorporate within them. When creating value objects, we can start small. For example, instead of using Integer for Age, we might create a class Age that offers methods such as isAdult, canDriveCar, ageInMinutes, ageInHours, and so on.

These small value objects bring more information and context to the provided value. The class name itself has a lot more data and also big room for reusability. Instead of running multiple services and spreading logic across the whole application, we have one single value object Age that has all the necessary logic. This logic can be utilized not only for the current instance but also for any future instances we create.

Entities

Quite similar in that regard are also entities. Despite being mutable, we can still benefit by having methods that fulfill business operations. In many cases, entities possess some sort of status. Let’s take as an example an entity Application.

    Public class Application {
        private ApplicationId applicationId;
        private Person person;
        private Status status;
        private LocalDate endDate;
        private LocalDate startDate;

        private Application(ApplicationId applicationId,
            Person person,
            Status status,
            LocalDate startDate) {
            this.applicationId = applicationId;
            this.person = person;
            this.status = status;
            this.startDate = startDate;
        }

        public static Application of(String name) {
            return new Application(ApplicationId.generate(),
                Person.of(name),
                Status.CREATED,
                LocalDate.now());
        }

        public void accept() {
            setStatus(Status.ACCEPTED);
            endDate = LocalDate.now;
        }

        public void refuse() {
            setStatus(Status.REFUSED);
            endDate = LocalDate.now();
        }

        private void setStatus(Status status) {
            this.status = status;
        }
    }

There are several interesting aspects happening. First of all, we control access with access modifiers for the constructor (exposing a static method of()). Additionally, we define methods for Applications to be accepted or refused. By hiding the setStatus method, we make sure that anyone handling instances of an application will know how to change status using an appropriate and self-explanatory method.

This example is only handling a small scope of fields, but for larger projects dealing with complex entities, defining such processes can help organize our code and show its purpose. There are more things that can be taken away from DDD into non-DDD projects, but introducing entities and value objects is simple and can enhance code comprehension. I suggest giving it a shot!

Conclusion

Domain-driven design, when used correctly, can help solve very complex problems. When discussing DDD, I rather refer to it as a philosophy of how the whole process of designing an application should look like, rather than purely focusing on developers’ perspective. There is a lot more to it, but with few principles in mind, domain-driven design can assist with software development.

There are numerous resources available, but one I would definitely recommend the most is a book written by Eric Evans – “DDD: Tackling Complexity in the Heart of Software”. In conclusion, even If we cannot take the whole DDD as a package, VOs and entities can still provide significant benefits!

4.5/5 ( votes: 6)
Rating:
4.5/5 ( votes: 6)
Author
Avatar
Szymon Sołtyk

Software engineer for over 5 years, utilizing best practices and delivering clean code on a daily basis. Currently working on automation of networking processes. After hours, an active time spending enjoyer, mostly playing squash.

Leave a comment

Your email address will not be published. Required fields are marked *

You might also like

More articles

Don't miss out

Subscribe to our blog and receive information about the latest posts.

Get an offer

If you have any questions or would like to learn more about our offer, feel free to contact us.

Send your request Send your request

Natalia Competency Center Director

Get an offer

Join Sii

Find the job that's right for you. Check out open positions and apply.

Apply Apply

Paweł Process Owner

Join Sii

SUBMIT

Ta treść jest dostępna tylko w jednej wersji językowej.
Nastąpi przekierowanie do strony głównej.

Czy chcesz opuścić tę stronę?