Immutable architecture



The topic of immutable architecture described here is part of my Applying Functional Principles in C# Pluralsight course.

In this post, I’d like to show a common approach to introducing immutability to your code base on an architectural level.

Immutability, State, and Side Effects

Before we start, let’s take a minute to define the terms. Most likely, you have already encountered them but I want to make sure we are on the same page here.

First of all, the term “Immutability” applied to a data structure such as a class means that objects of this class cannot change during their lifetime. There are several types of immutability with their own nuances, but they are not essential for us. For the most part, we can say that a class is either mutable, meaning that its instances can change in some way or another, or immutable, meaning that once we create an instance of that class, we cannot modify it later on.

Another important term is “State”. It indicates data that changes over time. It’s important to understand that state is not just data that comprises a class, state is a subset of this data that changes during its lifespan. An immutable class doesn’t have any state in that sense, only mutable classes do.

A side effect is a change that is made to some state. Usually, we say that an operation leaves a side effect if it mutates an instance of a class, updates a file on the disk or saves some data into the database.

Here’s an example that will help you bind all three concepts together:

public class UserProfile // Mutable, contains state

{

    private User _user;

    private string _address;

 

    public void UpdateUser(int userId, string name) // Leaves a side effect

    {

        _user = new User(userId, name);

    }

}

 

public class User // Immutable, doesn’t contain state

{

    public int Id { get; }

    public string Name { get; }

 

    public User(int id, string name)

    {

        Id = id;

        Name = name;

    }

}

The User class is immutable: all its properties are defined as read-only. Because of that, the only way for the UpdateUser method to update the user field is to create a new user instance and replace the old one with it. The User class itself doesn’t contain state whereas the UserProfile class does. We can say that the UpdateUser method leaves a side effect by changing the object’s state.

Immutable architecture

Methods with side effects make your code harder to reason about because of the dishonesty they bring to it. However, even if you try to impose immutability as much as possible, you cannot avoid them completely. Programming a system which doesn’t change would be impractical. After all, side effects is what software programs are supposed to do: they perform some calculations and they change their state in one way or another.

So how to deal with side effects? One of the techniques that help us to do that is separating the application’s domain logic from the logic of applying the side effects to the system’s state.

Let’s elaborate on that. It often happens that while processing a business transaction our code mutates some data several times from the point when the request comes into the application to the point where it is processed completely. This is a quite common pattern, especially in the world of object-oriented programming languages. What we can do instead is we can isolate the code that generates some artefacts from the code that uses them to change the system’s state:

Immutable architecture: Isolating the domain logic from side effects

Isolating the domain logic from side effects

The first portion then can be made immutable, we can free it up from any side effects.

Such approach gives us two benefits:

  • We simplify the domain logic as all code in it can be written in a functional way, using mathematical functions.
  • Such logic becomes extremely easy to test. The results of all operations are clearly defined in the methods’ outputs, so unit testing boils down to supplying a method an input and verifying its output.

Such architecture is an essential part of how most code bases written in functional languages operate. In the vast majority of cases, you have an immutable core which accepts an input and which contains all the logic for processing that input. This core is written using mathematical functions and for each request returns corresponding artefacts without mutating the system’s state:

Immutable architecture: Immutable core and mutable shell

Immutable core and mutable shell

And you also have a thin mutable shell which provides the input for the immutable core and which accepts the resulting artefacts and then saves them to the database. This shell comes into play when the core finishes working; it doesn’t contain any business logic.

Overall, the mutable shell should be made as dumb as possible. Ideally, try to bring it to the cyclomatic complexity of 1. In other words, try not to introduce any conditional operators (if statements) in it.

This approach also makes it natural to compose different sub-systems together:

Immutable architecture: Combining several cores together

Combining several cores together

We can take the core part and attach another component to it easily, due to its being immutable. Unit tests essentially act as such a component: instead of saving the artefacts to the database, unit tests verify them and then just throw away.

Immutable architecture example

An example here can be an audit manager class that keeps track of all visitors of some organization. It uses flat text files as an underlying storage:

Immutable architecture: Audit manager

Audit manager

The class is able to add new records to the log file, and remove information about the existing ones. It should also respect the maximum number of records a single file can contain and create a new one in case that limit is exceeded.

A straightforward implementation would look like this. Two methods which take some parameters and read and mutate the files directly:

public class AuditManager

{

    private readonly int _maxEntriesPerFile;

 

    public AuditManager(int maxEntriesPerFile)

    {

        _maxEntriesPerFile = maxEntriesPerFile;

    }

 

    public void AddRecord(string currentFile, string visitorName, DateTime timeOfVisit)

    {

        string[] lines = File.ReadAllLines(currentFile);

 

        if (lines.Length < _maxEntriesPerFile)

        {

            // Add a record to the existing log file

        }

        else

        {

            // Create a new log file

        }

    }

 

    public void RemoveMentionsAbout(string visitorName, string directoryName)

    {

        foreach (string fileName in Directory.GetFiles(directoryName))

        {

            // Remove records about the visitor from all files in the directory

        }

    }

}

Immutable versions of these methods, on the other hand, should clearly define their inputs and outputs:

public class AuditManager

{

    public FileAction AddRecord(

        FileContent currentFile, string visitorName, DateTime timeOfVisit)

    {

        // Return a new FileAction with the Update or Create action type

    }

 

    public IReadOnlyList<FileAction> RemoveMentionsAbout(

        string visitorName, FileContent[] directoryFiles)

    {

        // Exercise each of the directory files and return a collection

        // of actions that needs to be performed

    }

}

 

public struct FileAction

{

    public readonly string FileName;

    public readonly string[] Content;

    public readonly ActionType Type;

}

 

public enum ActionType

{

    Create,

    Update,

    Delete

}

 

public struct FileContent

{

    public readonly string FileName;

    public readonly string[] Content;

}

Here, the FileContent class fully encodes all information that our domain logic needs in order to operate properly. FileAction, on the other hand, contains the full set of instructions regarding what to do with files in the file system. They instruct the outside world about the actions that need to be taken.

AuditManager doesn’t perform those actions by itself, we need to introduce a Persister class for that purpose:

public class Persister

{

    public FileContent ReadFile(string fileName)

    {

        return new FileContent(fileName, File.ReadAllLines(fileName));

    }

 

    public void ApplyChange(FileAction action)

    {

        switch (action.Type)

        {

            case ActionType.Create:

            case ActionType.Update:

                File.WriteAllLines(action.FileName, action.Content);

                return;

 

            case ActionType.Delete:

                File.Delete(action.FileName);

                return;

 

            default:

                throw new InvalidOperationException();

        }

    }

}

Note that Persister is very simple, it just reads data from the file system and writes the changes back to it.

Finally, an application service should glue the two together:

public class ApplicationService

{

    private readonly string _directoryName;

    private readonly AuditManager _auditManager;

    private readonly Persister _persister;

 

    public ApplicationService(string directoryName)

    {

        _directoryName = directoryName;

        _auditManager = new AuditManager(10);

        _persister = new Persister();

    }

 

    public void AddRecord(string visitorName, DateTime timeOfVisit)

    {

        FileInfo fileInfo = new DirectoryInfo(_directoryName)

            .GetFiles()

            .OrderByDescending(x => x.LastWriteTime)

            .First();

 

        FileContent file = _persister.ReadFile(fileInfo.Name);

        FileAction action = _auditManager.AddRecord(file, visitorName, timeOfVisit);

        _persister.ApplyChange(action);

    }

}

This immutable version contains two pieces: an immutable core which holds all business rules and a mutable shell which acts upon the data the core generates. Such separation helps us get rid of side effects in the audit manager class completely. All methods in that class are mathematical functions, with clearly defined input and output. Persister works as a provider of input data for the manager and also as a consumer for the data it generates. The Application Service glues them together.

There are two best practices when it comes to creating an immutable architecture. First, make sure you separate the code that mutates the application’s state from the code that contains business logic. Make the mutable shell as dumb as possible. And second, apply those side effects as close to the end of a business transaction as you can.

For the most part, that is the way functional languages cope with side effects. The biggest and most important part of the system – its domain logic – is usually made immutable while the code that incurs side effects is simple and straightforward.

You can find the full source code of this example on GitHub. Here are quick links to the specific parts of it:

Share




  • Franco Lázaro

    Hi Vladimir, I started to follow you recently and I found your articles very useful and clear.

    I was trying to match your immutable architecture with DDD: would you say that the “Immutable Core” becomes “Core Domain” and “Services Domain”, and that “Mutable Shell” becomes “Application Services” and “Repositories” implementations?

    I like the idea behind immutability and functional programming, go on with this kind of posts!

    Greetings from Argentina

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Hi Franco, thanks, glad you found them interesting!

      That’s exactly correct. If you take the onion architecture diagram in the context of DDD, its inner core (Entities, Value Objects, Domain Events, and Aggregates), as well as Domain Services and (sometimes) Factories will form the immutable core, whereas Repositories and Application Services would belong to the mutable shell.

      http://i.imgur.com/NnpYQ65.png

      • Harry McIntyre

        Should the Domain Services not be in the central circle?

        • http://enterprisecraftsmanship.com/ Vladimir Khorikov

          In DDD, the core part of the domain model is entities along with value objects. Domain Services are part of the domain model too but they reside a bit further from the center.

      • scorelocity

        How can entities be made immutable?

        • http://enterprisecraftsmanship.com/ Vladimir Khorikov

          That’s a great question. I many cases, in languages like C# which don’t support immutability features out of the box, there’s no feasible way to make entities immutable.

          • scorelocity

            Thanks for the answer. I was thinking more conceptually. In DDD, isn’t an entity something you want/need to mutate?

          • http://enterprisecraftsmanship.com/ Vladimir Khorikov

            You are right. But to be precise, it’s not mutation that matters in entities, it’s the preservation of their identities. We can simulate mutation by creating new instances as long as we keep the Id for tracking purposes.

            In many languages, though, it’s easier to just mutate the entity and not bother with passing the identity around.

  • Rick Faaberg

    Thanks for the article! after watching your course I’ve been trying to put together a similar architecture for Web API. Something along the lines of Request comes in -> Request Handler -> Returns a Command -> CommandHandler that talks to the database, or whatever. Seems relatively straightforward how you presented it, but I’d like to get to a place where relatively little boilerplate “glue” code is required to route data between the manager and the persister. I’m not quite there yet. 🙂

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Seems relatively straightforward how you presented it

      Carefully choosing the example projects matters 🙂 Don’t worry if your version doesn’t look as smoothly, my production code doesn’t either.

  • Van Ly

    I see that there is an EventSource from this article. 🙂

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Good point, btw!
      Event sourcing is a good example of an immutable architecture implementation.

  • Miguel Bruno Gouveia

    In your code in GitHub you only implement tests for the immutable part. This is because you think it is not useful having unit tests for the persistent code or it has just to simplify the code?

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      This code is from my Pluralsight course where I didn’t want to shift the focus from the immutability topic and didn’t introduce integration tests because of that. I do find them useful, however, and I actually implemented one when I prepared this sample code. Here it is:

      public class IntegrationTests
      {
      [Fact]
      public void Application_service_adds_a_new_record()
      {
      string directory = Environment.CurrentDirectory;
      string logFilePath = Path.Combine(directory, "Audit_1.txt");
      File.WriteAllText(logFilePath, "1;Peter Peterson;2016-04-06T16:30:00");
      var service = new ApplicationService(directory);

      service.AddRecord("Jane Smith", new DateTime(2016, 04, 07, 12, 0, 0));

      string text = File.ReadAllText(logFilePath);
      Assert.Equal("1;Peter Peterson;2016-04-06T16:30:00rn2;Jane Smith;2016-04-07T12:00:00rn", text);
      }
      }

  • David Sackstein

    This implementation is inefficient because it reads files and write files on every AddRecord. Moreover, in order to evade primitive obsession, every time the file is read – to discover how many records it contains – each of the lines is parsed into an AuditEntry.
    Do you agree with the following alternative?
    Define an immutable State struct which has two properties: the name of the most recently written file and the number of lines in it. Store the current state in the AuditService class and update it in AddRecord and RemoveMentions.
    This may sound like a stateful solution, but it is not more stateful than the one presented. In the presented solution, the state is stored in the file system and is excusable because it is part of the mutable core. In this solution too, that state would be stored in memory and also stored in the mutable core.
    The State object itself would be immutable and would have a static NextState method that takes another and calculates the next one and returns it.
    The State class would be passed to the AuditManager which would invoke NextState. NextState would also be a natural candidate for a unit test.
    What do you think?

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Well, you still need to apply the changes on each `AddRecord` and `RemoveMentionsAbout`, not just store the state in the memory as the audit log change is the desirable side effect here. Also, I’m not sure that State containing just the name of the file and the number of lines in it would be helpful. `AuditManager` also needs the records themselves to work properly.

      I agree with you about performance inefficiency, though. To avoid reading the file all the time, you could store the state in the memory (represented by `FileContent`) but I would do it a bit differently. I’d leave `AuditManager` as is, add an `Apply` method to `FileContent` that would accept `FileAction` and return a new `FileContent`. The application service then would call

      _fileContent = _fileContent.Apply(action);

      along with calling

      _persister.ApplyChange(action);