What is Domain-Driven Design: The absolute beginners guide

This article is about domain-driven design, key concepts, vocabulary, and general ideas. I was motivated to write this article while attending the SymfonyWorld 2020 conference and seeing an inspiring lecture from Neal Brooks. You can use the Table of Contents to get a quick overview of this article and easily navigate to a part you are interested in.

Table of Contents

What is Domain Driven Design?

Firstly, when we ask what is Domain Driven Design we actually want to understand the concept of Domain-Driven design. It is an approach to understanding a problem, designing it, and coding it at the end. It results in better, more testable, and maintainable code.

Secondly, understanding what is domain-driven design will help you communicate with your entire team on a different level. Through this improved communication you will become a better developer, leader, or key stakeholder.

Finally, it is important to understand that this is not the only possible approach and it is not the single source of truth. However, if you do not know about it, you can not compare it with the others.

Domain Driven Design Concepts

Bound Context

Bound Context represents a meaning of the word within a different context. For example phrase “bounced” can have several meanings. In the context of stocks it might mean “prices bounced and increased”. On the other within the email delivery context, bounced has different meaning that “email bounced and was not delivered to the recipient”.

Before Domain Driven Design one of the common approaches was trying to define a model that represented complete domain. The problem in large domains is that it is difficult to correctly interpret words and meanings.

This is why identifying bound contexts makes sense.

For our example, we could have two contexts:

  • email delivery context – bounced emails
  • stocks context – bounced prices

For example, we might want to build a feature which introduces monitoring so we can track bounces. If we are not clear on the context, we do not know if we want to track bounced prices and to know when they change, or we simply want to track bounced emails and to have some track of unsuccessful email deliveries.

Ubiquitous Language

We already mentioned that bound context is critical, because it gives us precise vocabulary. This precise vocabulary is commonly called ubiquitous language. We can say that Ubiquitous Language is how people communicate in a specific group.

It is how people use language in that domain. We want to use this vocabulary when we build software, to keep it consistent. For example, we might say “User” while some other team says “Customer”. Furthermore, part of the team might use the term “Property” while other parts of the company might say “Instruction” for the very same thing.

Generally speaking, ubiquitous language should be consistently used throughout design process, task definitions, development, project documentation and code. If we are calling consumers of our application “Customers” we should use this both in documentation as well as variable names in our code.

If we want to understand domain driven design, it is important to understand the definitions and the vocabulary.

Domain and Domain logic

Commonly referred to as a business logic. Basically, this is a set of rules that define creation, processing and storage of your data. More importantly, business logic explains not only how but also why is the processing important in the exact way your business does it.

The domain is what our products is all about. This is what we sales is selling, what developers are developing, what product managers aim to improve and make better, what marketing is promoting. Generally, the domain is a playground within which company is creating a new value.

Domain represents knowledge, ideas, metrics, goals, data, reports, everything that contributes to the problem one company is trying to solve.

Finally, domain logic represents all the rules that helps companies to deal with complex business logic and to provide value to their customers.

Subdomain

Large domains can be represented as sets of subdomains. Each subdomain refers to different part of the business logic. For example, if we look at the ecommerce web site we might recognize different subdomains. There would be account management; supply chain management; orders and invoices; refund management etc.

This is just one example of subdomains, different projects and teams might have different priorities and segmentation.

Domain experts

Domain experts are people who will use our software and have experience in that area. They are people with relevant experience. This experience is important for software developers because they probably won’t have it.

Firstly, remember, making software is not about writing code. It is about building tools to help in doing some work. Because if we don’t know what kind of work is someone doing, we can not make a good tool.

Entity

An Entity is a class that has some sort of identity-making in unique from other identical objects. Commonly, this is a simple class definition, which has state and some rules regarding the life-cycle of that entity.

Commonly software code has many entities. If you look at the Subscribe feature mechanism you might have some of the following entities: Member, Subscription, Feature, Role.

Value Object

If we have object that can be immutable from the moment we initialize it, we have a value object. Similar to entity, value object is a class with some parameters holding values. However, unlike an entity that can change state, value object stays the same and has no important business logic for the software.

Commonly, value object is used to group information under a same class, but without complexity of introduction of new entity.

For example, if we need to store information about street address of the member, in PHP we might have something like this

// Class with only constructor
public class StreetAddress
{
	public function __construct(
	   private int $postalCode,
	   private string $street, 
	   private string $city
	) { }
}
$streetAddress = new StreetAddress(8000, "Streety Street", "New York");

Aggregate

Aggregate is a collection of objects that represent one concept in the domain. For example, a car is an aggregate of many different objects (design, motor, sales value, etc.).

The aggregate root is the entry-point into the specific aggregate.

Invariant

Invariants are business rules that the domain is enforcing. They are separate from application-level rules.

Domain Services

Domain services hold domain logic whereas application services don’t.

Application Services

Application services orchestrate the execution of domain logic according to outside-world input but don’t contain any domain logic themselves.

There are four layers of DDD architecture: domain/model; infrastructure (code, classes); application (application services), and UI (only talks with the application layer and does not know about domain or infrastructure).

Domain Driven Design

Domain-Driven Design is an approach to understanding the business in order to represent it in software, and also an architectural pattern for simplifying the business.

Relationship types in Domain Driven Design

We already know that domain driven design introduces different services existing within contexts. However, these services are not independent, meaning they have to coexist with each other. Furthermore, to coexist services have to communicate, to be in some kind of relationship with one another.

This section presents common patterns when it comes to relationship types in domain driven design. Different authors are representing relationship patterns in slightly different ways. However, all representations have similar focus and any of them is useful for understanding purposes.

We will present six relationship patterns combined in two major groups:

  • Cooperation Group (partnership, shared kernel)
  • Supplier Group (Conformist, Anti-corruption Layer, Open-host service)

Partnership

In partnership pattern, the integration between bounded contexts is happening ad hoc. This means, one team can notify a second one about any changes or breaking changes in the API and the second team will adopt, and vice versa.

This pattern works perfectly if teams are within the same company or if one team is developing multiple services and maintaining different bounded contexts.

Most important thing, coordination of integration efforts here is two ways. Meaning, it is open for discussion which service will change to keep the integration working.

Shared Kernel

Similar to partnership pattern, shared kernel is also a pattern from cooperation group. However, shared kernel is a more formal way to define a contract between bounded contexts. In this case, we have a shared kernel, which can be set of libraries, dependencies or services that both bounded contexts use.

Shared kernel is pattern that requires high levels of synchronization and very good communication channels between teams working on bounded contexts that have shared kernel.

Furthermore, in the case of one team owning multiple bounded contexts shared kernel is a good fit, because the team has awareness of all bounded contexts and responsibility to keep everything running and handling breaking changes.

Finally, the key to successfully implementing shared kernel is to keep the scope of the shared kernel as small as possible, limited only to what we need to accomplish with specific integration.

Conformist

Conformist is pattern from customer-supplier group where one of the bounded contexts – supplier provides service to the other bounded context – customer. We can say that supplier / service provider is “upstream” while consumer / customer is “downstream”.

In this case balance of the power goes in favor of upstream and the downstream team has to conform to the upstream team model.

To put it simply, service provider defines integration rules and consumer has to accept them if they want relationship to exist.

Anti-corruption Layer

Similar to the conformist pattern, in anti-corruption layer the balance of the power goes to the service provider “upstream”. However, in this case consumer “downstream” does not want to confirm with provider’s context, so there have to be some kind of compromise.

In this case, service provider might extend or modify their own bounded context with so called anti-corruption layer. This is basically a dedicated integration between service provider and a specific customer or group of customers that conforms to some bounded context.

There are some cases were anti-corruption layer is not a good idea:

  • If the “upstream” bounded context is poorly defined. Conforming and compromising with poorly defined context might result in poorly defined and poorly implemented anti-corruption layer. Emerging problems would outweigh perceived benefits.
  • If the “upstream” is changing a lot, these changes would be stopped at the anti-corruption middle-ware and customer would not be in sync with service provider.
  • If the “downstream” has sub-contexts or sub-domains this introduces complexity. If we are not aware of all the facts finding a middle ground via anti-corruption layer can be risky.

Open-host Service

Open Host Service is a pattern that represents cases where the balance of power goes toward the “downstream” / consumers. The “upstream” / service provider is interested in keeping consumers happy and providing the best possible service.

To protect them from implementation changes service provider decouples implementation model from the public interface. This way, public interface is staying unchanged or carefully versioned while implementation model can change without risk for the consumers.

Common example of this approach would be most of the OpenApi services where service provider converts its internal business logic / internal bounded contexts into a public interface in the form of the protocol that is convenient to the consumers.

If the open host service uses well known languages for the integration such as JSON, GraphQL, XML etc, this is called published language.

Separate Ways

Sometimes, when we analyze integration between two services we might discover it to be of a little value. In this cases we do the opposite of the relationship and we decide to go separate ways by deciding that business contexts no longer have connection and no longer interact.

Common example of separate ways would be migrating from one service provider to the other. While we are creating some of the above mentioned relationships with a new provider, we are most likely separating ways with the old provider that we will no longer use.

Some of the common reasons to go on a separate ways:

  • Communication issues – if the teams are not able to communicate and collaborate
  • Generic subdomains – if the subdomain in question is generic, it might be more cost effective to simply replicate it than to invest in integration and maintaining this integration
  • Model differences – sometimes models can be so different that none of the relationship approaches is either possible of feasible. Sometimes it is better to simply duplicate the functionality instead of keeping the integration alive.

Summary

Domain-Driven Design is an approach on how to understand the specific business and how to successfully represent it with the software solution. Domain driven design gives us vocabulary, concepts and patterns to better accomplish communication between different stakeholders and to have common way of designing and developing business functionalities.

Firstly, some of the most important concepts in domain driven design are Bound Context (BC), Ubiquitous Language, Domain and Domain logic, Subdomain, Domain experts, Entity, Value Object, Aggregate, Invariant, Domain Services and Application Services. With bound context being one of the most important concepts.

Once we understand that bound context is specific to how one team represent for example one service, we understand that relationships between bound contexts are as important as relationships between services.

Secondly, Domain Driven design provides six relationship patterns combined in two major groups: Cooperation Group (Partnership, Shared Kernel) and Supplier Group (Conformist, Anti-corruption Layer, Open-host Service). If it is no longer feasible to keep the integration between two services, than these services and bound contexts can Separate Ways.

Thirdly, it is important to work with domain experts to discover different rules and hidden requirements. Think about words and communication between teams and business. Furthermore, if possible, identify bounded contexts as early as possible.

Finally, start talking about business processes and invariant with all stakeholders. However, understand that there is no genually the “right way” of doing things. Domain-driven design is about understanding the problem and using that understanding to make concuss and reasonable decisions.

If you are interested in Domain-Driven Design there are lots of good books about it. My suggestion would be Domain-Driven Design: Tackling Complexity in the Heart of Software or something more practical Implementing Domain-Driven Design