Learning Domain-Driven Design (DDD)

Learning Domain-Driven Design (DDD)

Build and design software that can easily adapt and be maintained.

Introduction

I'm reading the book Learning Domain-Driven Design by Vlad Khononov. I can tell by reading the beginning and its reviews, it is probably the best book on Domain-Driven Design.

What's exciting about it is that the author himself explains in the beginning how he struggled to learn Domain-Driven Design and understand how things connect.

Domain-Driven Design

Domain-driven design (DDD) is a software design approach that focuses on the domain. It involves creating a shared understanding of the domain among all stakeholders, including developers, business analysts, and domain experts, and using that knowledge to guide the design and development of the software.

This way we can build software design for the specific needs of the business and its users and can be easily extended and maintained.

DDD can be divided into two parts: strategic and tactical.

Strategic Design

The strategic tools of DDD are used to help align the design of the software with the business goals and objectives, and to guide the development process.

Analyzing Business Domains

A business domain refers to the specific area or problem that a company is operating in.

To achieve its business domain's goals and targets, a company has to operate in multiple subdomains. A subdomain is a specific area that has its unique characteristics and requirements. There are three types of subdomains: core, generic and supporting.

Core

A core subdomain is what sets a company apart from its competitors, such as inventing new products or optimizing processes for cost savings.

Generic

Generic subdomains are complex business activities that are performed in the same way by all companies and do not provide a competitive edge. These subdomains require battle-tested implementations, as innovation or optimization is not necessary.

Supporting

Supporting subdomains support the business but don't provide any competitive advantage compared to the core subdomains.

Discovering Domain Knowledge

Communication and knowledge sharing are vital for a software project to succeed. Engineers need to understand the business domain and share the same models as the domain experts to design and build great software that can adapt to the needs of the business.

Model

A model is a simplified representation of a thing or phenomenon that intentionally emphasizes certain aspects while ignoring others. Abstraction with a specific use in mind.

An effective model is a map. It doesn't contain all the details, but enough to fulfill its purpose.

[...] it might be worth-while to point out that the purpose of abstracting is not to be vague, but to create a new semantic level in which one can be absolutely precise.

Ubiquitous language

Domain-driven design introduces a tool to help with this: The ubiquitous language. A language that all stakeholders use to communicate.

The language by domain experts shouldn't be translated into something else for developers to understand. Domain experts, developers, and everyone else should communicate and understand the context the same way through the language.

When cultivating a ubiquitous language, we are effectively building a model of the business domain. The model is supposed to capture the domain experts’ mental models—their thought processes about how the business works to implement its function.

Tools such as wiki-based glossaries and Gherkin tests can help with the process of documenting and maintaining a ubiquitous language.

Managing Domain Complexity

Whenever we find a conflict in domain experts' mental models, we need to break down the ubiquitous language into separate bounded contexts. A common language should be consistent within its bounded context, but the same terms can have different meanings across different bounded contexts.

Breaking down the ubiquitous language into bounded contexts is a design decision, while subdomains are discovered.

A team can own multiple bounded contexts, but a bounded context can only be worked on by a single team, not multiple ones to avoid misunderstandings and misconceptions.

Each bounded context’s lifecycle is decoupled from the rest. Each bounded context can evolve independently from the rest of the system. However, the bounded contexts have to work together to form a system.

The size of your bounded contexts will depend on your specific domain. Sometimes it is clearer to have a small bounded context, sometimes it won't make sense, and a larger one is more befitting.

Integrating Bounded Contexts

Bounded contexts can be implemented and worked on independently. On the other hand, they have to integrate to form the end system which is our goal.

These are the contracts, the points where the bounded contexts have to collaborate.

In the simplest scenario, all of the bounded contexts are being worked on by a single team. The other scenario is multiple teams working on different bounded contexts of the systems. There are two ways different teams can go about this collaboratively: partnership and shared kernel patterns.

Partnership

Whenever changes in one of the bounded contexts happen that affect the other one, the teams communicate in an ad-hoc fashion. They clear things out and collaborate right away.

This is befitting for teams in the same or similar time zones since it happens synchronously.

Shared kernel

The bounded contexts overlap. They share the necessary parts that are being used in both contexts. Any change in this scope needs to be immediately reflected in both the bounded contexts.

Shared kernel

You may want to have CI or something similar that makes sure whenever something in the shared kernel changes, it re-runs the necessary checks such as the tests in both bounded contexts (if not more).

This is befitting when the cost of duplication is higher than the strong coupling the shared kernel introduces.

Customer-supplier

A customer-supplier relationship is an upstream-downstream relationship. One acts as a customer, the other one as a supplier. Most of the time we have an imbalance of power, but both teams can succeed independently.

Customer–supplier relationship

Three patterns: the conformist, anticorruption layer, and open-host service patterns.

The conformist

Here the downstream bounded context adjusts to whatever the upstream one offers. The upstream context can evolve fast while the downstream one has to adjust whenever changes in the upstream's context affect the downstream's context. This pattern favors the upstream context.

Conformist relationship

Anticorruption layer

This pattern still favors the upstream, but the downstream bounded context is not willing to conform.

Integration through an anticorruption layer

Instead, through an anticorruption layer, it will translate the upstream bounded context’s model into a model that fits its needs.

Open-Host Service

This pattern favors the consumers. The supplier provides a public interface that is decoupled from its context, meaning the public interface and the supplier can evolve independently.

The public interface is not using ubiquitous language, but rather a published language, a language that the consumer understands.

Integration through an open-host service

Context map

A context map is a diagram we can create to understand how the different bounded contexts integrate. It demonstrates:

  • High-level design

  • Communication patterns

  • Organizational issues

It should be implemented from the beginning and updated as the integration changes so it always reflects the current state.

Tactical Design

Tactical design is about the how. It's about how things get implemented, more on a technical level.

Implementing Simple Business Logic

Two patterns that are suitable for simple business logic such as supported subdomains: Transaction Script and Active Record.

Transaction Script

This pattern is used to organize business logic within a system as straightforward procedural scripts. The procedures ensure each operation is a transaction, either it fails or succeeds, and there are no states in the middle.

Active Record

This pattern is used when the business logic is still simple but you need to operate on complicated data structures, you can then implement those data structures as active records. An active record object is a data structure that provides simple CRUD data access methods.

Tackling Complex Business Logic

A domain model is an object model of the domain that contains both behavior and data. DDD’s tactical patterns are the building blocks of such an object model:

  • Aggregates

  • Value objects

  • Domain events

  • Domain services

Value objects

Specific things in the business domain that can be modeled as values should be value objects. A value object is a model for a specific value, it contains both the data and behavior: methods for manipulating the data. Value objects are not unique and are immutable.

An example would be a phone number.

Aggregates

An aggregate is a hierarchy of entities. An entity compared to a value object is always unique. It is defined by its identity, instead of its attributes. It can consist of value objects. Entities are used to model real-world objects. An example would be a person.

The data included in an aggregate’s boundary must be strongly consistent to implement its business logic. The data in an aggregate can only be modified through its public interface.

The aggregate acts as a transactional boundary, therefore, all of its data has to be committed to the database as one atomic transaction.

Domain events

Domain events get published by aggregates. This allows aggregates to communicate with external entities that can subscribe to these events.

Domain services

Domain services are stateless objects used to encapsulate business logic that is not specific to any aggregates or value object. They are usually used to perform operations that involve multiple different aggregates or value objects.

Modeling the Dimension of Time

When you need to have deep insights into the system, you can use the event sourcing pattern to model the dimension of time in the domain model’s aggregates.

In an event-sourced domain model, all changes to an aggregate’s state are stored in an event store as a series of domain events. The system can be represented as the sum of its past events.

Benefits of an event-sourced domain model:

  • Time traveling

  • Deep insight

  • Improving auditing and debugging

Architectural Patterns

Architectural patterns are about how we organize our codebase and split up the responsibilities. Three patterns are introduced in this chapter.

Layered Architecture

In essence, this pattern organizes the codebase into three layers:

  • Presentation layer: The part that trigger's the program's behaviors, both synchronous and asynchronous.

  • Business logic layer: The part that contains the business logic. This is the heart of the software.

  • Data access layer: This is the area of persistence. Its meaning is broader than a database. It could be cloud storage for an instance.

Ports & Adapters

This pattern is also called hexagonal architecture. This is because the business logic gets moved to the center instead of depending on another layer beneath it.

Remember how we mentioned that business logic is the heart of the software, in this type of architecture it truly shines as the heart of the software, everything evolves around it.

  • Ports represent the interface between business logic and external dependencies

  • Adapters implement the ports and handle communication with external dependencies

Ports & adapters architecture

Command-Query Responsibility Segregation (CQRS)

This pattern separates the reading and writing responsibilities of an application. We have the read and command sides.

  • The command execution model is responsible for the writing of data

  • The read models are responsible for the reading data

Since reading and writing are separated, we have the projection engine in the middle. Its responsibility is to provide an up-to-date view of the data to the read side of the application. By keeping the projection in sync with the data on the command side, the projection engine ensures that the query side always has access to the most current version of the data.

CQRS architecture

Communication Patterns

This chapter was quite broad. It introduced multiple different patterns for integrating a system's components.

  • Model translations to implement anticorruption layers or open-host services.

    • Stateless translations happen on the fly, as incoming (OHS) or outgoing (ACL), requests are issued.

    • Stateful translations are more complicated since they require a database.

  • Outbox pattern to reliably publish aggregates' domain events. It's reliable because it stores a record of each domain event that needs to be published in the database.

    Image of the Outbox pattern:

    Outbox pattern

  • Saga pattern to implement simple cross-component business processes, these would be processes for an instance across multiple bounded contexts.

Image of the Saga pattern:

Saga

  • If you're dealing with a more complex scenario where the saga pattern wouldn't be befitting (conditional cases), then use the process manager pattern.

Image of the process manager pattern:

Trip booking process manager

Applying Domain-Driven Design In Practice

Design Heuristics

A heuristic is a rule of thumb. It's something we can use as a guide, but it doesn't mean it is always right. The author in this chapter dives into when to use a pattern, architecture, and on top of that what testing approach is befitting for each.

The tactical design decision tree:

Tactical design decision tree

Evolving Design Decisions

Change is constant. We have to embrace change and adapt to stay competitive as a business. As the domain evolves, we have to change the design of the system to match the current business needs.

The organizational structure could also happen along the way. We then have to design the software in a way that fits the communication and cooperation between the new teams.

  • If a subdomain's functionality increases, find finer-grained subdomains to make better design decisions.

  • Avoid letting a bounded context try to do too many things. Make sure that the models within a bounded context are only used to solve specific problems.

  • Keep your aggregates as small as possible. Use the heuristic of strongly consistent data to find opportunities to move business logic into new aggregates.

EventStorming

EventStorming is a workshop a team can do to discover and learn the domain. The team won't just end up with bounded contexts, but they will learn a lot from each other through discussions and knowledge-sharing.

  • Build a ubiquitous language

  • Model the business process

  • Explore new business requirements

  • Recover domain knowledge

The session takes 2 to 4 hours to complete. Refer to the book for each step if you ever decide to do it. The steps should be used as a guide, but you can tweak it to whatever fits your team's needs.

An image of how the Miro/Whiteboard will end up looking at the end of the session:

A possible decomposition of the resultant system into bounded contexts

Domain-Driven Design in the Real World

This chapter is about practically applying domain-driven design. Not everyone will be experts on DDD and DDD is not a thing where you either go all-in or nothing.

The first step to applying DDD should be to analyze the organization’s business domain and its strategy.

When applying the tools DDD offers, we have to do so gradually. Big rewrites are too big of a risk. It is better to take safe and incremental steps.

EventStorming is great to build the foundational shared understanding of the domain and a ubiquitous language. We can also use it to constantly rediscover knowledge and to understand things as they evolve.

Relationships To Other Methodologies And Patterns

Microservices

Instead of having the entire system be a service, microservices break down the entire system into multiple smaller services. At first, this seems nice, since each service would have a specific focus, hence fewer reasons to change. This is also nice if you've multiple teams in an organization, working on different parts of the system.

Integration complexity

However, this doesn't always reduce the complexity. We have two complexities we need to take into account here:

  • Global complexity: The complexity of the entire system. All microservices in the end work together to form the entire system.

  • Local complexity: The complexity of each service. The narrower the focus of the service is, the less complexity.

Too many microservices increase the global complexity, while an entire system has a high local complexity. We need to find a middle ground here.

Service granularity and system complexities

Deep Modules

Microservices should be deep. The concept of Deep Modules comes from the book A Philosophy of Software Design. Deep services encapsulate the local complexity and reduce the global complexity.

Deep modules

A good heuristic is to align services with the boundaries of the subdomains.:

  • Subdomains focus on the what

  • Subdomains are naturally deep

  • Subdomains contain use cases that are strongly related to each other

Subdomains

Bounded Contexts

Microservices and bounded contexts are often used interchangeably. This is a confusion. They have separate meanings, however, all microservices are bounded contexts, but this isn't always the case the other way around.

The system can be decomposed into bounded contexts that are wide, meaning they have too many functionalities to be a microservice. Let's not forget, microservices are strictly physical boundaries.

Granularity and modularity

Event-Driven Architecture (EDA)

In EDA, the system's components communicate with one another through asynchronous event messages. A component will either have subscribers or be subscribed to another component.

Asynchronous communication

There are two types of messages:

  • Event: A message describing a change that has already happened

  • Command: A message describing an operation that has to be done

Events

As for the events, there are three types of events:

  • Event notification: This notifies the subscribers that an event has happened, however, for additional information about what changed the subscribers have to make a subsequent query for the data. The good part here is that the data is strongly consistent.

  • Event-carried state transfer (ECST): This tells the subscribers not just what happened, but includes all necessary data. It can be more than data that changed. The data ECST contains can be cached by the subscribers. The drawback here is that data is eventually consistent. However, this removes the need for the subsequent query.

  • Domain event: This event describes changes in the producer's business domain. Domain events are somewhat in the middle between event notifications and ECSTs. However, domain events intend to model and describe the business domain. Integration with other components isn't expected as compared to event notifications.

Example of how the different objects could look taken from the book:

eventNotification = {
    "type": "marriage-recorded",
    "person-id": "01b9a761",
    "payload": {
        "person-id": "126a7b61",
        "details": "/01b9a761/marriage-data"
    }
};

ecst = {
    "type": "personal-details-changed",
    "person-id": "01b9a761",
    "payload": {
        "new-last-name": "Williams"
    }
};

domainEvent = {
    "type": "married",
    "person-id": "01b9a761",
    "payload": {
        "person-id": "126a7b61",
        "assumed-partner-last-name": true
    }
};

Pitfalls

The author brings up three pitfalls to be aware of that he has seen happen in practice:

  • Implementation coupling: This happens when the subscribers are subscribed to a component and coupled to their implementation details. Meaning, there is a public interface provided to decouple the implementation details of the producer from its subscribers.

  • Functional coupling: This happens when multiple subscribers to a producer always have to project the state the same way every time they receive the event message.

  • Temporal coupling: This happens whenever something needs to happen in a strict order for this to work.

Good example

A good example of EDA done well. It doesn't include the pitfalls mentioned:

Refactored system

It's beautiful. The components are decoupled and the state is already projected. This gives each component more autonomy. Less stress and mental overload of what needs to be done or fixed.

Heuristics

Heuristics for implementing EDA:

  • Assume the worst possible things that could happen.

  • Use public and private events. Be cautious of exposing implementation details. Make sure OHS uses the bounded context's published language.

  • Evaluate consistency requirements to determine which type of event is befitting.

Conclusion

This book was phenomenal. It's beginner friendly and dives deeper than just scratching the surface. It's practical too.

I loved it, gotta be the best book on DDD out there.