Functional DDD with C# Part 1: the benefits of functional thinking

Functional DDD with C# Part 1: the benefits of functional thinking

Series introduction

DDD (Domain Driven Design) and functional programming have become increasingly popular to master the complexity of our modern applications.

But how DDD tactical object patterns (entities, aggregates, value objects) apply in functional programming? And how to do it in C# which is an object language? Lots of questions I searched for and found answers to.

I will explain my functional DDD C# journey throughout this series. I wrote a small library for that: Grenat.Functional.DDD. I hope you will learn new useful techniques for your daily work to write less and more sustainable code.

Let's dive in!

Why you should adopt functional thinking

Functional programming is not complicated

A few words first about functional programming. Of course, it is a very large topic. But if we focus on its more important concepts, it is not so complicated. It is just an all-rounder programming style based on functions and on the avoidance of side effects.

Every developer knows what a function is and every developer knows what a side effect is. So everyone can use it. Functional programming is just a rigorous procedural programming style. It is another way to think about our programs, which is why I like to talk about "functional thinking".

Functional thinking is applicable in any language

What I find interesting is that the fundamental principles of functional thinking are simple and can be used in many languages. In an object language like C# or Java, it is even possible to find a subtle balance between object and functional thinking.

Say goodbye to unwanted side effects

Let's go back to side effects. For a refresher, a function does a side effect if it modifies a state outside of its local state. It can be something like sending an email or persisting data in a database. A side effect can also be more localized, like modifying an object attribute or a function parameter.

Here is a simple side effect that can happen everywhere, especially in large teams and large codebases: the reference parameter is modified by accident.

var names = new List<string>() {"Emmanuel", "Justin", "Joe"};

void DoASideEffectByAccident(List<string> names) 
{
    // do something...
    names.Add("Donald");
    // do something...
}

DoASideEffectByAccident(names);

Console.WriteLine(names); // Output : Emmanuel, Justin, Joe, Donald

We will see in the rest of the article how functional thinking will help to avoid this problem.

However, a program has no interest if it does not perform a side effect. So what is functional thinking for? Well, we will use functional thinking fundamentals at the core of our programs while other layers will be in charge of performing side effects.

The fundamentals of functional thinking

Pure functions

A pure function is a function that produces the same result when called multiple times with the same parameter.

This function is not pure because when called several times it will produce a different result:

int NewRandom() {
    var r = new Random();
    return r.Next();
}

The following function is also unpure because it calls the database: there is no guarantee that at its next call you will get the same result.

public static async Task<Price> GetProductPrice(DbContext context, int id)
{
    return await context.Product.AsNoTracking()
                        .Where(p => p.ProductId == id)
                        .Select(p => Price.Create(p.Price))
                        .FirstOrDefaultAsync();
}

And finally, this other RemoveItem() function is also unpure because if modifies one of the Cart's attribute. Also, if it is called multiple times, the result will be different because her behavior depends on a shared state (the Items and CartTotal attributes):

public class Cart
{
    public List<CartItem> Items { get; private set; }
    public int CartTotal { get; private set; }

    public int RemoveItem(int cartItemId, int units) 
    {
        Items.RemoveAll(i => i.ItemId == cartItemId);
        return Items.Sum(i => i.Price);
    }
}

The behavior of a pure function depends exclusively on its input parameter and therefore:

  • It doesn't produce side effects,

  • It is easily testable,

  • It is parallelizable.

Immutability

Immutability is one of the most important principles of functional programming. It requires a "copy on write" discipline, i.e. you have to make a copy of the object before modifying its data.

Avoiding side effects

The biggest advantage of immutability is that it avoids creating accidental side effects. When a developer modifies an existing code, he no longer has to identify all its potential side effects. Combined with pure functions, immutability allows the developer to focus only on the code that he needs to modify. It saves time, energy and avoids accidents.

A simple action is to use immutable collections using a C# immutable list:

var immutableNames = ImmutableList.Create("Emmanuel", "Justin", "Joe");

int DoSomethingWithoutSideEffect(ImmutableList<string> names) {
    // With Immutable Lists, a copy of the modified object is returned when calling the Add function, thus avoiding a side effect.
    var newNames = names.Add("Donald");

    // Do something...
    return result;
}

DoSomethingWithoutSideEffect(immutableNames);

Console.WriteLine(immutableNames); //Output : Emmanuel, Justin, Joe

More predictable code in multi-threaded environments

Immutable code is automatically thread-safe because we can only read the values. And concurrent reads by multiple threads pose no problems.

Honest functions

An honest function (or a total function) is a function that for any value of a starting set matches a value in the destination set.

This function is not honest (it is partial) because it has no matching result in the integer destination set for a negative parameter:

public static int AddOne(int number)
{
    if (number <0) 
        throw new ArgumentException($"{number} cannot be negative.");
    else 
        return number++;
}

Honest functions are less restrictive than pure functions: a function that performs a side effect like an input/output but which in any case returns a value of the declared type in its signature is honest.

However, writing honest functions requires a mindset change: you must forget exceptions and favor tangible results in case of an error. Exceptions should only occur in exceptional cases as its name suggests. In C#, the Fluent Results library is perfectly suited for writing honest functions: it avoids throwing exceptions.

Some other benefits of functional thinking

Make unit testing easier

By creating a maximum of pure and honest functions, we make the writing of automated tests easier: it is enough to compare the function output with what is expected.

On the other hand, if your code base contains procedures that produce side effects, creating automated tests becomes more difficult since:

  • You have to test the side effect,

  • You may have to mock the class to test an outcoming interaction (with a database for example).

Of course, testing side effects would still be necessary because each application produces side effects. But you have to minimize them to the maximum.

Less code in critical parts of the codebase

In functional programming, we use declarative programming: we write what the program should do. Imperative programming remains however necessary because almost all processors work this way: they are designed to execute sequences of elementary instructions.

For example, SQL is a declarative language. Take the following query:

Select Sum(amount) Order_total from Order 
Inner join OrderLine on OrderLine.Id = Order.Id
Where Order.Id = @OrderId

I did not write all the necessary loops because the declarative statement was translated into a set of imperative statements by the compiler: the execution plan. A similar example could be made with .Net Linq to Objects query: the compiler generates all the imperative code for us.

Here is another example: adding a product in the orchestrating layer of the application (see part 6 of this series). This code deals with all of the error cases without writing any conditional "glue":

var p = await Product.Create(addProductDto.Reference, addProductDto.Name, addProductDto.Price, addProductDto.FamilyId)
        .BindAsync(VerifyProductReference, () => CountProductReferences(addProductDto.Reference))
        .BindAsync(VerifyFamilyId, () => CountFamilyIds(addProductDto.FamilyId))
        .MapAsync(SaveProduct);

In these examples, we wrote very little code which drives to produce fewer bugs. Nevertheless, do not believe that you'll write less code everywhere in your codebase with functional thinking. Some parts of your application will indeed require to write more code, especially to deal with immutability.

What's important is that functional thinking allows you to write less code in the critical parts of your codebase: the domain and application layers (see next post).

Less interfaces boilerplate code

In the object paradigm, developers tend to create too much interfaces. Sometimes to abstract things that will finally have only one implementation. But sometimes we really need to write interfaces to abstract an out-of-process resource like a database. The good news is that we can get rid of most of them using functional thinking: just inject functions.

Let's compare the object and functional approaches to write a repository. Both are perfectly suitable depending on your needs.

Let's begin with the classical object approach:

public interface ICatalogRepository 
{
    public Task<List<Product>> GetProducts();

    public Task SaveProduct(Product product);

    /*...*/
}

Now we need to implement this interface:

public class CatalogRepository : ICatalogRepository
{ 
    public Task<List<Product>> GetProducts() 
    {
        /*...*/
    }

    public Task SaveProduct(Product product)
    {
       /*...*/
    }
}

Eventually, you will have some extra work with your dependency injection tool:

Services.AddScoped<ICatalogRepository, CatalogRepository>();

Then, pass an ICatalogRepository as a parameter in a Save method:

public void async Save(ICatalogRepository catalogRepository, Product product) 
{
    /*Do some verifications before saving product in database...*/
    await catalogRepository.SaveProduct(product);
}

Now, let's switch to a more functional approach. You can write a static function that performs the save in the database :

public static Task SaveProduct(DbContext context, Product product)
{
   /*...*/
}

Then, inject it in a Save method:

public static async void Save(Func<DbContext, Product, Task> SaveProduct, DbContext context, Product product) {
     /*Do some verifications before saving product in database...*/
    await SaveProduct(context, product);
}

As you can see, the functional approach can save you some work: less boilerplate interface code, no dependency injection mechanism to deal with and easier testing with on-the-fly declared lambda functions. Think about it before writing interfaces.

Some drawbacks

Watch out for external libraries

Some libraries may not be compatible with the fundamentals of functional thinking such as immutability. This is particularly the case of EF Core which needs write Therefore, avoid calling external libraries in your functional core as much as possible.

Some additional work to deal with immutability

Since dealing with immutability implies making copies of data structures to modify them, more work has to be done on the constructors:

  • Default constructors must be banned,

  • Depending on the call context, you may need to create several constructors to take into account the optional parameters,

  • It is necessary to check all the invariants in constructors to ensure the created data structure is valid.

Immutability also involves more work in client code: creating a new object to modify it involves more code than directly updating it. This will be especially true for more complex data structures like DDD aggregates.

Immutability also means being constantly vigilant. For an object to be truly immutable, all of its attributes must be immutable. Particular care should be taken with reference types: collections, objects, etc. If one of a class's attributes is mutable then that class is not immutable.

Performance that may be a little worse

Creating a new instance of an object to modify it creates a performance hit, especially on immutable collections in C#. But these problems will only appear if you manipulate large amounts of data or perform complex calculations. In this case, it will be necessary to create optimized code by putting aside immutability.

Summary

  • Functional thinking is simple and can be seen as a rigorous procedural programming style. It's a new way to think about our programs.

  • Functional thinking is applicable in any language. If it is an object language, it is possible to benefit from both paradigms.

  • Functional thinking brings a synergy of benefits to master our complex modern applications.

  • Be careful with external libraries that could be incompatible with the functional thinking fundamentals.

  • Some functional thinking fundamentals like immutability may require you to write more code, so use it wisely: in your core business areas, on features that require high reliability or in complex features.

About the cover image

Messier 94, the Croc-eye galaxy, is located in the Canes Venatici constellation, about 16 million light-years from us. Its heart is extremely brilliant: it is a starburst galaxy. It means it exhibits an exceptional new star formation rate compared to other galaxies.

See it in large size on my portfolio.