Home

tietokone.io

Musings on software development, remote working and engineering leadership

Blog About

Writing simple code in practice with domain driven design

January 16, 2025

In my previous post, I introduced (shamelessly plagiarised) the idea of simple code. I outlined four rules-of-thumb which, in my experience, serve as useful guides when doing web development:

  • The implementation should be the simplest thing that works for your use-case
  • Decisions should be deferred to the last responsible moment
  • Every dependency, class, and layer of indirection should be justified by a concrete problem
  • Code should be refactored continuously

So far, so architectural, but what do they mean at the code-level?

The domain’s the thing

Martin fowler describes Domain Driven Design as “cent[ring] the development on programming a domain model that has a rich understanding of the processes and rules of a domain.” It’s an approach to Object Oriented Programming that focusses on organising code into classes which map to the business domain, and is particularly well-suited to web-services and other transactional code. Fowler identifies three key components of DDD:

  • The ubiquitous language
  • Domain classes: entities, values and services
  • Bounded contexts.

As I hope to demonstrate, these three things dovetail nicely with the simple code rules-of-thumb I defined earlier.

Domain classes and the ubiquitous language

A fundamental requirement of DDD is a common, unambiguous language shared between the developers and users of the system. This necessitates the code being organised into classes which map directly to domain concepts that will be familiar to users. For example, in an e-commerce system, such classes might include catalogue items, orders, invoices, etc.

In his seminal work, Domain Driven Design, Eric Evans categorises domain classes into three types:

  • Entities: these have distinct identity that runs through time, and potentially different representations; they usually map to records in a database or NoSQL equivalent, and include business concepts like the aforementioned catalogue items, orders and invoices
  • Values: essentially the ‘primitives’ of the domain model, such as dates, monetary values, addresses, etc.
  • Services: these handle interactions with the outside world, and bootstrap the business logic contained by the entities; controllers in REST APIs are a good example

This approach, done well, encapsulates the concept of ‘the simplest thing that works,’ at least at the class level. Each domain class corresponds to a real-world item or concept which needs to be modelled by the system. There are no superfluous classes.

Keeping this rule-of-thumb in mind helps guide developers away from the common antipattern of the Anaemic Domain Model, in which “there is hardly any behavior on [the domain] objects, making them little more than bags of getters and setters… Instead there are a set of service objects which capture all the domain logic, carrying out all the computation and updating the model objects with the results.” Fowler neatly summarises the accidental complexity introduced by this approach:

In essence the problem with anaemic domain models is that they incur all of the costs of a domain model, without yielding any of the benefits. The primary cost is the awkwardness of mapping to a database, which typically results in a whole layer of O/R mapping. This is worthwhile iff you use the powerful OO techniques to organize complex logic. By pulling all the behavior out into services, however, you essentially end up with Transaction Scripts, and thus lose the advantages that the domain model can bring.

Consider the following code:

using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
using Dapper;

public class Repository
{
    private readonly IDbConnection _dbConnection; // conection lifecycle handled by the DI container

    public Repository(IDbConnection dbConnection)
    {
        _dbConnection = dbConnection;
    }

    public async Task<IEnumerable<Order>> GetOrdersAsync()
    {
        return await _dbConnection.QueryAsync<Order>("SELECT * FROM Orders");
    }

    public async Task<Order> GetOrderByIdAsync(int id)
    {
        return await _dbConnection.QueryFirstOrDefaultAsync<Order>(
            "SELECT * FROM Orders WHERE Id = @Id",
            new { Id = id });
    }

    public async Task<IEnumerable<Invoice>> GetInvoicesAsync()
    {
        return await _dbConnection.QueryAsync<Invoice>( "SELECT * FROM Invoices");
    }

    public async Task<Invoice> GetInvoiceByIdAsync(int id)
    {
        return await _dbConnection.QueryFirstOrDefaultAsync<Invoice>(
            "SELECT * FROM Invoices WHERE Id = @Id",
            new { Id = id });
    }
}

public class Order
{
    public int? Id { get; set; }
    public int CustomerId { get; set; }
    public int ProductId { get; set; }
}

public class Invoice
{
    public int? Id { get; set; }
    public int OrderId { get; set; }
}

This is a good example of an anaemic domain: the two entity models have no behaviours at all, and we have an ever-growing Repository ‘god-class’, which really only exists so we can use dependency injection for the IDbConnection. Rewriting this using proper domain driven design, we arrive at the following:

using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
using Dapper;

public class Order
{
    public int? Id { get; set; }
    public int CustomerId { get; set; }
    public int ProductId { get; set; }

    public static async Task<IEnumerable<Order>> GetAllAsync()
    {
        return await Context.WithDbConnectionAsync(db => db.QueryAsync<Order>("SELECT * FROM Orders"));
    }

    public async Task<Order> GetByIdAsync(int id)
    {
        return await Context.WithDbConnectionAsync(db => db.QueryFirstOrDefaultAsync<Order>(
            "SELECT * FROM Orders WHERE Id = @Id",
            new { Id = id }));
    }
}

public class Invoice
{
    public int? Id { get; set; }
    public int OrderId { get; set; }

    public async Task<IEnumerable<Invoice>> GetAllAsync()
    {
        return await Context.WithDbConnectionAsync(db => db.QueryAsync<Invoice>( "SELECT * FROM Invoices"));
    }

    public async Task<Invoice> GetByIdAsync(int id)
    {
        return await Context.WithDbConnectionAsync(db => db.QueryFirstOrDefaultAsync<Invoice>(
            "SELECT * FROM Invoices WHERE Id = @Id",
            new { Id = id }));
    }
}

/// <summary>
/// DI containers don't play well with models,
/// so we use a static context instead to provide access to the dependencies.
/// </summary>
public static class Context
{
    public static DbDataSource Database { private get; set; } // set during app startup in Program.cs

    /// <summary>
    /// Performs actions on a database connection and then closes the connection.
    /// </summary>
    public static async Task<TResult> WithDbConnectionAsync<TResult>(Func<IDbConnection, Task<TResult>> handler)
    {
        await using var connection = await Database.OpenConnectionAsync();
        return await handler(connection);
    }
}

The god-class is gone, the connection lifecycle is handled simply without a DI container, and each entity encapsulates the data and behaviours of a single domain model.

Bounded in a nutshell

Another key property of good Domain Driven Design is the Bounded Context. As a system becomes ever more complex, it becomes increasingly difficult to maintain a single coherent domain model. Classes become larger, and an antipattern emerges: separate groups of properties and methods within a class which always get used together. SOLID principles prescribe Interface Segregation to solve this, but DDD instead suggests breaking up the system into multiple subdomains, which are separate from each other. A good use-case for this is when you have both an internal API driving the frontend and an external API used by customers to build their own integrations. Whilst these domains conceptually overlap, it often makes sense to separate them - there could be different data formats, pagination logic, etc.

This ties in nicely with the advice to defer decisions to the last responsible moment. It is (usually) impossible to a priori break up a domain context into sub-contexts, but it becomes clear at some point that the cost of maintaining a set of growing classes is higher than the cost of refactoring them. This leads good teams to ‘clean up their mess’ (and violate the open/closed principle in the process).

Conclusion (there’s still no silver bullet)

Domain Driven Design works well for web development, since much of what web-services does is transactional (CRUD operations on database records, etc.), and both SQL and NoSQL databases lend themselves to modelling domain entities. Keeping the code simple means avoiding the curse of the Anaemic Domain. Of course, DDD is not a universally-applicable set of principles. Many problems are a poor fit: DevOps/infrastructure scripting (which tends to favour resource-based models), or Single Page Applications (which favour organising code into pages and components). But the approach has helped me to keep my web services and APIs simple and maintainable. For anyone interested in digging into this, Martin Fowler’s blog is a great place to start.