Simple Domain-Driven Design – Building Blocks

If you are wondering what Domain-Driven Design (or in short DDD) is, why it is so useful technique and how it is applied this post will explain all if this. We will be working bottom up by looking at each DDD building block at a time and iteratively connecting them together in a cohesive mental picture. If you would like to learn more, at the end there are links to fundamental DDD and Design books so you can continue exploring.

In code example below we will be using C#. It’s statically strongly typed programming language with automatic memory management, hence it is a great “tool for the job” we need to do, expresses domain model, relationships and interfaces in a simple way with no language-specific quirks. Knowledge of C# is not required to understand the code examples. General ability to read code is enough. Code examples can be found here.

So without further delay let’s get started!

What is DDD?

Domain-Driven Design is an architectural style which suggests to use business domain terminology in application code, organize code in a way that corresponds to how business is structured and operates, to bring developers close to business people so they finally begin to communicate effectively with each other. Business people can unambiguously explain processes and rules to developers and developers can explain what code is doing to business. It is a set of recommendations and guidelines which help developers and architects to manage code and architecture complexity and yet be able to express any complicated business rules and processes in a simple and easy to understand way. It is set of techniques and patterns that allow software systems to grow and change naturally and in harmony with business. How is that possible?

Ubiquitous Language

Business people know the domain, they understand intricate business processes and workflows, they know the market and develop strategies for the business to succeed. They need technology, but they don’t speak “developers” language, rather every business domain has its own terminology.

Developers, from the other hand, know technological aspects best. They (should I say we?) design and build complex software, think about scalability, maintainability, security and other *ability or non-functional requirements. We love to write code to bring ides to life. We also speak our own language which sounds like gobble-gobble to other people. Think about explaining a recent challenge you faced at work to a “non-technical” friend.

Wouldn’t it be great to have both business and development folks communicate effectively and unambiguously so that business can explain the vision and developers can bring it to life at the right time and with the right functionality to the market? Sounds like a great idea, does’t it?

What is Ubiquitous Language?

Ubiquitous Language is a solution to communication problem. It’s a fundamental building block of Domain-Driven Design. It breaks the wall between business and development and bridges the gaps between these two distinct populations. So what is it exactly? It is well established terminology, the way people name things and processes. It doesn’t have to be industry-wide terminology though. Every company is likely to have its own domain vocabulary. If we are building a product for external customers we should use terminology the external customers understand. If we are building integration between systems then our API should speak the language of our clients.

Ubiquitous Language
Ubiquitous Language is foundation for DDD

How to apply?

Common language or Ubiquitous Language should be used while naming variables, functions, classes, modules and so on. Functions or procedures should describe actions or commands that are possible in the business domain using Ubiquitous Language. Modules structure may correspond to business domains or subdomains structure.

To see how we can apply Ubiquitous Language we need other DDD building blocks and a business domain. The domain needs to be simple and familiar to all. Let’s choose a domain first.

Imagine we are designing a business to business system which helps restaurants to handle their day-to-day activities. The system will handle supply of ingredients and inventory tracking, customer transactions (checks), salaries, accounting and other financials. It will help restaurant owners with suggestions to improve business and run it more efficiently.

The little paragraph above contains a lot of business vocabulary already. Let’ see how we can use it while designing and coding our application.

Entity (or Reference objects)

Entity is a domain object that has an identity which is concentrated in a small subset of attributes or even a single attribute. The most commonly used example of a single identity attribute is Id. Identity attribute(s) survive over time while other attributes may change over the life cycle of an Entity. Non-identity attributes of an Entity object carry only descriptive meaning. They do belong to the Entity object, but do not form its identity.

My personal blueprint to check whether a domain object is an Entity is to think about how to compare two objects of the same kind. In other words, how do we implement an equality operator. Let’s demonstrate it using Transaction class that has two operators == and !=. In order to compare two transactions we only check whether they have same Id. The other attributes are not relevant while comparing, because they only belong to Transaction object, hence do not identify it.

public class Transaction
{
    public Guid Id { get; set; }

    public decimal Amount { get; set; }
        
    public Currency Currency { get; set; }

    public PaymentType PaymentType { get;set; }

    public DateTime OccurredOn { get; set; }

    // Implementing Equality operator
    public static bool operator ==(Transaction a, Transaction b) => a.Id == b.Id;

    // Implementing Non-Equality operator
    public static bool operator !=(Transaction a, Transaction b) => a.Id != b.Id;

    // Need to override object.Equals()
    public override bool Equals(object obj)
    {
        return obj is Transaction transaction &&
                this == transaction;
    }

    // And also need to override object.GetHashCode()
    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

Entity objects can be referenced just by identity attributes. This leads to another way of thinking about Entities. Imagine you need to find an entity object among other of the same kind only by non-identity attributes. If you are lucky there will be a single entity found, however what if there are multiple… Which is the one we are looking for? For example, think of searching a Transaction by Amount and Currency. There may be multiple transaction for the same amount and currency. However if we search by identity attribute(s) we will find a single Entity and it is guaranteed to be the one we need. For example, searching Transaction by Id which is guaranteed to be unique.

Entity or Reference Object

Properties of Entities are the following.

  • Entity is identifiable by a single or a subset of attributes.
  • Identity attribute(s) are guarantees to uniquely identify an Entity object among its kind.
  • Identity attributes do not change over time. Other attributes may.
  • Two Entity objects with the same identity attributes is the same Entity, regardless of other attributes.

Value object

Value object is very easy to comprehend. Imagine a class where all properties identify the whole object. It doesn’t matter that two objects may have different memory addresses, they are equal as long as all attributes are equal. Comparing to Entities, let look at a Payment class below.

public class Payment
{
    public decimal Amount { get; set; }

    public Currency Currency { get; set; }

    public PaymentType PaymentType { get; set; }

    // Implementing Equality operator.
    public static bool operator ==(Payment a, Payment b) =>
        a.Amount == b.Amount &&
        a.Currency == b.Currency &&
        a.PaymentType == b.PaymentType;

    // Implementing Non-Equality operator
    public static bool operator !=(Payment a, Payment b) => !(a == b);

    // Need to override object.Equals()
    public override bool Equals(object obj)
    {
        return obj is Payment payment &&
                this == payment;
    }

    // And also need to override object.GetHashCode()
    public override int GetHashCode()
    {
        return HashCode.Combine(Amount, Currency, PaymentType);
    }
}
Value Object

To check whether two Payment objects are the same we compare all properties of both objects. That is the key difference between Value objects and Entities. Going to extremes we can think about standard data types like int, bool, double and etc. a Value Objects.

Service

Services are doers of Domain-Driven design. Some of domain operation don’t naturally fit into Entities and Value Objects. Instead of artificially forcing that we can factor these operations into domain Services.

Service

Domain services need to be highly cohesive and have their domain purpose defined clearly. They use Ubiquitous Language for operation names and because services are doers, having a verb in their name is a good practice,

Services do not hold any state or data. They are transient. They do things and may also return result.

Services are part of a domain, however is important to distinct however domain services from infrastructure services.

Let’s look at the example below. We have domain service PaymentProcessor, which purpose is to process Payments and record payments as Transactions. See how it uses Ubiquitous Language, implements domain logic and works with domain objects.. From the other hand, IElectronicTransactionProcessor and ICheckClearingService used by PaymentProcessor are completely infrastructure services. They do useful but generic things and definitely don’t belong to our domain.

public class PaymentProcessor : IPaymentProcessor
{
    private readonly IElectronicTransactionProcessor _electronicTransactionProcessor;
    private readonly ICheckClearingService _checkClearingService;

    public PaymentProcessor(
        IElectronicTransactionProcessor electronicTransactionProcessor,
        ICheckClearingService checkClearingService)
    {
        _electronicTransactionProcessor = electronicTransactionProcessor ??
            throw new AccessViolationException(nameof(electronicTransactionProcessor));
        _checkClearingService = checkClearingService ??
            throw new ArgumentNullException(nameof(checkClearingService));
    }

    public Transaction ProcessPayment(Payment payment)
    {
        var transaction = new Transaction(payment);

        switch (payment.PaymentType)
        {
            case PaymentType.Cash:
                transaction.CompleteTransaction();
                break;
            case PaymentType.Check:
                _checkClearingService.BeginClearingCheck(transaction.Payment);
                transaction.BeginProcess();
                break;
            case PaymentType.Visa:
            case PaymentType.MasterCard:
            case PaymentType.AmericanExpress:
                _electronicTransactionProcessor.BeginProcessTransaction(transaction);
                transaction.BeginProcess();
                break;
        }

        return transaction;
    }
}

Aggregate

Think of aggregates as abstractions which control access and enforce invariant for a cohesive group of Entities and Value Objects. Creating an Aggregate is similar to drawing a boundary around a cluster of Entities and Value Objects and selecting a single Entity which will play a role of a primary or root entity. Root entity has global identity. Entities within an Aggregate boundary have local identity. Outside the boundary they have no identity as all.

Aggregate and root Entity

Invariant

Aggregate takes care of maintaining its own invariant. All business rules, validation rules and state transitions are controlled by an Aggregate. This is achieved simply by implementing interface in an Aggregate’s root Entity and exposing the interface to outside world. The interface describes state, what can be done or happened to an aggregate as a whole, of course using ubiquitous language. 🙂

Access control

Access to an Aggregate from the outside world is conducted only through the root Entity. Aggregate can expose other non-root objects to the outside world, but that should not allow to break internal invariant of the Aggregate. From the outside world’s perspective the references to Aggregate’s internal Entities and Value objects should be treated as transient, meaning used withing a single operation and then discarded. All of this can be achieved in two ways.

  1. By convention. Everybody in a team agrees not to abuse references to internal Aggregate’s objects and not to “hack into” Aggregate by using internal objects.
  2. By enforcement. Code does not expose true references to internal objects, but rather a projections or deep copies. It would be impossible or very hard to break Aggregate’s invariant by manipulating internal objects. Any changes to projections or copies will not impact the Aggregate.

Garbage collection

When deleting an Aggregate all internal Entities and Value objects need to be deleted as well. Non of the internal Entities have global identity, therefore they do not have any meaning without the Aggregate. When using No-SQL document database this may be an easy one, just delete a document. If we have highly normalized relational database schema, we will need to take care of cleaning up all tables.

Example

To understand Aggregates better, let’s look at an example. We continue working an a restaurant app. This time we are designing functionality to take table orders. A servant takes an order from a table, once completed, submits it to the kitchen for execution. When order is prepared it gets served. When guests are ready to pay, the payment is processed and order gets closed. To implement this domain logic we created two classes.

TableOrderItem

TableOrderItem class represents a single order item which contains menu item price and count. Guests can have multiple orders of the same dish (menu item), so the count property of TableOrderItem represents how many times same menu item has been ordered. TableOrderItem is a Value Object so we make it immutable to protect its properties by making setters private hence we can safely pass it outside of an Aggregate.

public struct TableOrderItem
{
    public TableOrderItem(MenuItem menuItem, int count)
    {
        _ = menuItem ?? throw new ArgumentNullException(nameof(menuItem));
        PricePerOne = menuItem.Price;
        Count = count;
    }

    public decimal PricePerOne { get; private set; }

    public int Count { get; private set; }

    // C# simplified syntax for a property with a getter only.
    public decimal TotalPrice => PricePerOne * Count;

    /*
    * Equality operators and methods overrides below.
    */

    public override bool Equals(object obj)
    {
        return obj is TableOrderItem item &&
                PricePerOne == item.PricePerOne &&
                Count == item.Count;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(PricePerOne, Count);
    }

    public static bool operator ==(TableOrderItem a, TableOrderItem b) =>
        a.PricePerOne == b.PricePerOne &&
        a.Count == b.Count;

    

TableOrder

TableOrder is a root Entity of our Aggregate that tracks a table order’s life-cycle. For simplicity we defined table order’s life-cycle as series of transitions. When we just create an order it is in open state. Then a servant takes individual orders from each guest and adds order items to the table order. Then the order is submitted to the kitchen. When order is prepared by the kitchen it is served and after gusts are full and happy a payment is processed and the order is closed.

Because we design TableOrder class as root Entity or our Aggregate it needs to protect the invariant and restrict access to the Aggregate internals. In order to achieve that we are going to use some nice “intend revealing features” of C# language and .NET class libraries.

  • We close write access to TableOrder properties by marking setters as private.
  • Because making setter private does not protect collections from being modified outside, we return immutable projection of OrderedItems collection instead of direct reference. This allows the outside world to iterate through order items, but does not allow to modify the collection and break the Aggregate’s invariant.
  • We internally keep track of all menu items being added to the table order, validating invariant before every operation and implement other domain logic. We do not expose any of this logic outside of the Aggregate boundaries.
  • TableOrder exposes coherent interface using public properties and methods.The interface is sufficient for the outside world to work with TableOrder Aggregate.

The code below should be self-descriptive. Comments are put in place to help understand the code and correlate it with everything we talked above.

public class TableOrder
{
    // Internal collection of items made private.
    private readonly Dictionary<Guid, TableOrderItem> _items =
        new Dictionary<Guid, TableOrderItem>();

    // Status is exposed with only public getter.
    public TableOrderStatus Status { get; private set; } = TableOrderStatus.Open;

    // Property with getter only.
    public decimal TotalBalance => _items.Values.Select(toi => toi.TotalPrice).Sum();

    // Exposing a readonly projection to items collection.
    public IReadOnlyCollection<TableOrderItem> OrderedItems => _items.Values.ToImmutableList();

    /*
    * Below is code that implements domain logic.
    * It also performs all validations to ensure the invariant.
    */

    public void AddOrderItem(MenuItem menuItem)
    {
        _ = menuItem ?? throw new ArgumentNullException(nameof(menuItem));

        if (Status != TableOrderStatus.Open)
        {
            throw new InvalidOperationException("Unable to add order item as it's not open.");
        }

        if (_items.TryGetValue(menuItem.Id, out TableOrderItem orderItem))
        {
            _items[menuItem.Id] = new TableOrderItem(menuItem, orderItem.Count + 1);
        }
        else
        {
            _items.Add(menuItem.Id, new TableOrderItem(menuItem, 1));
        }
    }

    public void RemoveOrderItem(MenuItem menuItem)
    {
        _ = menuItem ?? throw new ArgumentNullException(nameof(menuItem));

        if (Status != TableOrderStatus.Open)
        {
            throw new InvalidOperationException("Unable to remove order item as it's not open.");
        }

        if (_items.TryGetValue(menuItem.Id, out TableOrderItem orderItem))
        {
            if (orderItem.Count <= 1)
            {
                _items.Remove(menuItem.Id);
            }
            else
            {
                _items[menuItem.Id] = new TableOrderItem(menuItem, orderItem.Count - 1);
            }
        }
    }

    public void SubmitToKitchen()
    {
        // Implementation details are skipped for the sake of brevity.
        Status = TableOrderStatus.SubmittedToKitchen;
    }

    public void CompleteKitchenPreparation()
    {
        // Implementation details are skipped for the sake of brevity.
        Status = TableOrderStatus.KitchenPrepared;
    }

    public void Served()
    {
        // Implementation details are skipped for the sake of brevity.
        Status = TableOrderStatus.Served;
    }

    public Transaction ProcessPaymentAndCloseOrder(Payment payment)
    {
        if (payment.Currency != Currency.USD)
        {
            throw new UnsupportedCurrencyException(
                $"{payment.Currency} is not supported for payments.");
        }

        if (TotalBalance > payment.Amount)
        {
            throw new InsufficientPaymentException(
                "Payment is too small to cover table order balance.");
        }

        var transaction = new Transaction(payment);
        transaction.CompleteTransaction();
        Status = TableOrderStatus.Completed;
        return transaction;
    }
}

Factory

When an Aggregate is simple enough the client code can just use Aggregate’s root Entity constructor to create new instance. The constructor can create all necessary internal object structures that are needed to maintain the invariant. Over time when the Aggregate evolves, it’s internal structure can grow and construction can become a process of it’s own. This may overwhelm the Aggregate’s root Entity and blend it’s direct responsibilities with construction code that is very technical by nature and typically does not correspond to any of domain model concepts.

Factories come to the rescue. They encapsulate construction code helping to create Aggregates maintaining invariant.

Factory

Factories carry the following properties.

  • Factory construction operations must be atomic. Single factory method must create an Aggregate that maintains its invariant and integrity.
  • Factories should not be coupled to concrete types they create, but rather to abstractions. If we have a hierarchy of inheritance or interface implementations for an Aggregate, a factory should return an abstract class or an interface instead of concrete root Entity class.
  • Factories become coupled to their arguments as arguments directly impact how Aggregates are created.

There are several ways to implement factories. The simplest is to use root Entity constructor. It’s OK when when construction is simple. Another is to use factory Method pattern and implement a static interface in a root Entity class. To facilitate concretion of elaborate hierarchies of classes we can use Abstract Factory pattern. If we have multiple steps in creation process we can use Builder pattern, however any combination of builder’s steps should produce an Aggregate with valid invariant. Details about Factory implementation patterns can be found in Design Patterns: Elements of Reusable Object-Oriented Software book by the Gang of Four.

Repository

We have our Entities, Value Objects and Aggregates defined, we can create our elaborate structures, so now the question is how do we persist all of this? Given a TableOrder root entity with multiple table order items added to it how do we save out aggregate so that next time we can retrieve and continue working with it? The answer as you may have guessed is Repositories.

Repository

Storage access code whether it’s relational or No-SQL database, whether we use any ORM or not can get complicated and very technical. This code would less likely be written in ubiquitous language or express domain concepts. When writing database access code people may fall into a trap of thinking in database tables terms, designing model as just a set of data container classes for table data. This hurts domain model wiping out all advantages it brings.

Repositories encapsulate storage access code and provide clear interface to retrieve, store and remove domain objects. The interface is designed with domain concepts in mind and ubiquitous language is use in describing it. This allows repositories to be naturally integrate-able into domain model.

Repositories provide access to Entities and Value objects which have global identity. Think of having a repository per one or multiple closely related Aggregates. This way repositories help to keep Aggregate’s access and invariant safe. Because otherwise ad-hoc database queries, if used to access and alter any domain objects, break Aggregate’s paradigms and introduce mess into domain model.

Repositories encapsulate database access code which makes it possible to mock a repository interface for the purpose of unit tests. Also, if storage needs to be changes, we will just need to create new repositories implementing same interface, but internally accessing other storage. Domain model would not need to be changes.

Repositories may take advantage of Factories when reconstituting instances of objects from the storage, especially when construction is quite involved process.

Take a look at ITableOrderRepository. It is an example of how a Repository public interface may look like. It allows to perform CRUD operations on TableOrder Aggregate as well as perform necessary search operations.

public interface ITableOrderRepository
{
    TableOrder FindTableOrder(Guid tebleOrderId);

    TableOrder SaveTableOrder(TableOrder tableOrder);

    void DeleteTableOrder(TableOrder tableOrder);

    IEnumerable<TableOrder> FindAllTableOrders(TimeInterval period);

    IEnumerable<TableOrder> FindServantTableOrders(Guid servantId, TimeInterval period);
}

Module

We, humans, carry awesome machinery in our heads, our brain. However even though it’s amazing in many ways, it doesn’t come without flaws. Our brain can hold from 3 to 7 things at a time and is best to do one thing at a time. Context switch is also expensive and time consuming operation. When we design and write code we need to consider our weaknesses and factor code in a way that is easy to comprehend. Modules is a mechanism to divide code into coherent chunks and allow fellow developers to zoom into a module and observe the concept in more details as well as zoom out and see entire picture without being distracted by all implementation details.

What makes a good module in terms of Domain-Driven Design? There are so many ways to divide code but general rules of high cohesiveness and low coupling apply regardless. Let’s look at 2 different ways how we can divide our restaurant app into modules and learn from the examples.

Infrastructure based approach

If we have multi-layered application we can create a module per layer. For example.

  • User Interface
  • Domain Logic
  • Services
  • Data Access

However, there is a problem with this approach. Imagine we are working on a task (or a user story) that requires to make a change to TableOrder workflow. Where do we find code responsible for that? Well, with current structure of our modules the code is scattered across many modules. Our Entities and Value Objects are in Domain Logic module. Our Services are in corresponding module as well. Our TableOrderRepository is in Data Access module. We need to visit at least three different modules to understand how TableOrder works so we can make required changes.

Domain model approach

Now let’s look at another modules structure.

  • User Interface
  • Domain Layer
    • Table Order (Entities, Value Objects, Services, Repositories, Factories)
    • Payments (Entities, Value Objects, Services, Repositories, Factories)
    • … other sub-domains
  • Infrastructure Layer
Module

In this approach we just need to focus on a single modules to make changes to TableOrder. We can also zoom-out and observe all sub-domains in our Domain Layer just by looking at modules. Now Modules become part of domain model that reveal its structure.

Summary

I hope this post helped you to work through Domain-Driven Design building blocks and understand the main objectives and reasoning behind DDD. If you would like to learn more and dive deeper you will find resources below extremely useful.

If you have any further questions, suggestion, feedback or simply kudos, please leave a comment 🙂

Resources to continue exploring

Domain-Driven Design: Tackling Complexity in the Heart of Software, by Eric Evans. Great book to start with DDD. Explains complicated concepts in easy to understand fashion with real-life examples.
Implementing Domain-Driven Design, by Vaughn Vernon. Goes deeper into DDD and focuses on applying DDD while designing and implementing software.
Design Patterns: Elements of Reusable Object-Oriented Software, by the Gang Of Four. Very well structured book that describes veriety of patterns, where to apply and how to implement each of them. It is a fundamental book for every Software Engineer, even if the one does not practice Object Oriented Programming. A small downside might be that code examples are in C++ and Smalltalk, however clear description and UML diagrams make it easy to comprehend even without knowing the languages above.

Posts created 29

10 thoughts on “Simple Domain-Driven Design – Building Blocks

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top