Implementing an Entity

After completing the code for all Value Objects held by a customer, we can revisit the DDD-friendly model of the Customer Entity itself, which will impose one more invariant addition to all the invariants Value Objects are encapsulating. The domain expert requires the customer to be mature enough and should have an annual gross income of at least 80K.


abstract class Entity<TId> where TId : IEquatable<TId>
{
    public TId ID { get; }

    public Entity<TId>(TId id)
    {
        Id = id;
    }
}

public class Customer : Entity<TId>
{
    public CustomerName Name { get; }
    public USHomeAddress Address { get; }
    public Email Email { get; }
    public DateOfBirth DateOfBirth { get; }
    public AnnualIncome AnnualIncome { get; }

    private Customer(CustomerID id, CustomerName name, USHomeAddress address, Email email, DateOfBirth dateOfBirth, AnnualIncome annualIncome) : 
        base(id)
    {
        this.Name = name;
        this.Address = address;
        this.Email = email;
        this.DateOfBirth = dateOfBirth;
        this.AnnualIncome = annualIncome; 
    }

    private const uint MIN_AGE = 18;
    private const decimal MIN_INCOME = 80_000;

    public static Customer Create(CustomerID id, CustomerName name, USHomeAddress address, Email email, DateOfBirth dateOfBirth, AnnualIncome annualIncome, DateTime currentDate)
    {
        if (dateOfBirth.GetAge(currentDate) < MIN_AGE )
        {
            throw new ArgumentException($"Customer must be at least {MIN_AGE} years old.", nameof(dateOfBirth));
        }
        if(annualIncome.Value < MIN_INCOME)
        {
            throw new ArgumentException($"Annual gross income should be at least ${MIN_INCOME}$.", nameof(annualIncome));
        }

        return new Customer(id, name, address, email, dateOfBirth, annualIncome);
    }
}

public record struct AnnualIncome
{
    private const decimal MinIncome = 100;
    private const decimal MaxIncome = 100_000_000;

    public decimal Value { get; }

    public AnnualIncome() : this(decimal.Zero) { }

    private AnnualIncome(decimal value)
    {
        if (value < MinIncome || value > MaxIncome)
        {
            throw new ArgumentException($"Annual income must be between {MinIncome}$ and {MaxIncome}$.", nameof(value));
        }

        Value = value;
    }

    public static AnnualIncome Create(decimal value)
    {
        return new AnnualIncome(value);
    }

    public override string ToString()
    {
        return Value.ToString("C"); // Format as currency
    }

    public static implicit operator decimal(AnnualIncome annualIncome)
    {
        return annualIncome.Value;
    }

    public static explicit operator AnnualIncome(decimal value)
    {
        return Create(value);
    }
}

Notice how the Entity differs from the Value Object. It possesses an identity represented by some ID that is a characteristic that does not change throughout the entity's life, whether it mutates or not. We stressed that fact in our domain model by introducing an Entity base class. Entities can be tracked and referenced by their IDs from other entities. That is also why the ID should be equitable to at least say whether IDs represent the same entity object or not.

Entity also strengthens generic weaker invariants of Value Object by putting additional constraints on top of them. The goal here is to be as profuse with constraints as possible. More constraints lead to less uncertainty and more robust code. The more limiting concepts you bring from the real-world domain into the code, the less opportunity you leave for misinterpretation or malfunction.

The same rule applies here that the coder domain model should be pure, i.e. technology agnostic, without side effects or external dependencies such as logging, system clock, emails, databases, network calls, IO system, communication bus, benchmarks, alerts, frameworks, etc. Prefer packing all the sensible invariants into immutable Value Objects and doing all the state transitioning in Entities. Follow a functional style of programming wherever possible and make your methods honest. I will elaborate on those topics a bit later.

The point of the domain model layer is to give you absolute freedom from the technical aspect of your programming language and executing environment so you can remain unbiased and focused solely on domain knowledge and the “offline” business you are creating your solution for. The more logic you can squeeze into that layer the better as you automatically increase the level of control over your code. That code is highly testable, debuggable, readable, and easy to work because accidental complexity is at a minimum here.

You might yell, hmm, how in the hell I am going to be efficient technically so that the domain model runs fast on modern hardware, and provides runtime transparency by leveraging logging, telemetry, health checks, heart beats? You want async/await, multi-threading, structured logging, service bus, serialization, security/authorization management, storage, communication, and so on. The quick answer those concerns have no place in the domain model and generally get offset to the outer boundaries of the system but that does not mean that the domain model should be completely ignorant of those pure technical use cases. Yes, you should not embed the features above in the declaration of your entities or value objects because those features don’t have real-life double or analog in a real domain. Still, you can provide technology-agnostic hookups that can be leveraged in those technical scenarios.

Yes, domain logic should be synchronous and thread-unsafe, i.e. free of any multithreading or asynchronous/delayed processing/synchronization but it can be implemented granular enough for parallelism orchestrated by the application layer. Yes, we don’t add explicit loggers or telemetry publishers but we provide domain events that can be intercepted and logged on upper layers so we can understand what is going on in the domain layer. Yes, we don’t have methods in entities to save or get them from a database but we have application services “repositories” to do so efficiently. Many more things are not allowed in the core domain to keep it clean, fast, reliable, and easy to work with but in reality, it is not limiting you in your technology stack. You will still be able to add nicely all the other aspects and concerns but do it in a specific way so you don’t compromise the purity of the domain model with extrenious code.

If the core domain, i.e. the heart of the system is designed right you should be able to observe and track the harmony of the code modeling it. When the code is written correctly following the rules I am going to be talking about here you should feel immediate satisfaction, fulfillment, and confidence in the reliability of your work. If you feel the opposite, i.e. something smells or gut feeling puts you in slight discomfort, that is a sign you are missing something your frame is shaky and you can’t build a reliable structure on top of it. The appreciation of symmetry is in our nature. I would argue that we are born with it. Every symmetry is perceived as beautiful by our mind and the beauty represents the most stable constructions in the universe. Look at all those natural biological ornaments, fractals, spirals, Fibonacci sequences, or golden ratios. Those are the bricks of perfection and the more such bricks you incorporate into your software solution, the more adequate and robust it should be.

The canonical domain model should look like a hierarchy of objects where limitations/invariants are added from the leaf to the root layer. The root is usually represented by an entity that is called an aggregate root in domain-driven design. The aggregate is the smallest group of interconnected entities maintaining transactional consistency. The invariants in the aggregate are held at all times and changes are applied atomically over all objects included in that aggregate. The hierarchy (“tree“ like) is a very convenient structure well understood by our mind as it finds manifestation in almost every object in nature that is why it is beneficial to stick to the “tree” hierarchy in the domain model.

Also, psychologists say we can’t operate with more than 7 objects in our memory at a time so I take that number as a rule of thumb when designing a system. Every modeled concept should not consist of more than 7 constituents. Methods should not do more than 7 decision making, entities/value objects should not contain more than 7 properties, given aggregate (group of objects bound by certain rules or limitations) should be comprised of more than 7 of other constituents, a context (“bounded context“ in DDD terms) should not contain more than 7 aggregates.

I will expand on the concepts that did not find detailed review in this article later. For now, I would like to linger on a very important aspect of software design which is Validation. I will talk about validation in the next article and show where it belongs in relation to the core domain.

0
Subscribe to my newsletter

Read articles from Dmitry Dezuk (Dezhurnyuk) directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dmitry Dezuk (Dezhurnyuk)
Dmitry Dezuk (Dezhurnyuk)

Senior Software Developer with over 10 years of performance-focused .NET development experience. I assist developers in solving architectural challenges and simplifying complex software projects. Writing as Dmitry Dezuk to share everyday productivity tips for developing faster and more reliable software.