Immutability in DTOs?

Something that comes up every so often is the question of whether or not Data Transfer Objects should be immutable - that is, should our design of the classes and types of a DTO enforce immutability.

To answer this question, we first need to look at what purpose a DTO serves. As the name explicitly calls out, it is an object that is used to carry data between processes. In practice, we don't literally transport an object back and forth between processes, and instead there is some form of serialization.

Where the waters get muddied is when types are used to describe message contracts - whether in asynchronous communication in the form of messaging or APIs. Given our usage of DTOs these days, why might we want to enforce immutability?

Benefits of immutability

The primary benefit of immutability is, well, you can't change the object! That makes a lot of sense on the receiving side of a communication - I shouldn't be able to change the message, even it's a deserialized version of that message.

There are some cases where I might want to be able to change a message as it flows through the system. For example, with a Document Message, I often pass the same message between many endpoints, and each endpoint processes the message and adds its own information as it goes along, sometimes with an attached Routing Slip.

This is rarer case, however, and what often winds up happening is I'm not mutating the original message, but the deserialized copy of the message, arriving in the form of the object.

By far the majority of cases are the receiver should not modify the message upon receipt. Sounds great! So why don't we actually do this?

Immutability in a mutable world

The main issue with immutability is that it depends on your language, platform, and even tooling, to support this.

In my systems, the default message content type is application/json, and the senders/receivers are .NET/C# or JavaScript/TypeScript. The main hurdle is that neither of these languages, or the platforms and tooling, support immutability out-of-the-box.

In order to declare an Order message as immutable, it's fairly complex. You have to make sure all write paths are initialized on construction:

public class Order {
  public Order(Customer customer, List<LineItem> lineItems) {
    Customer = customer;
    LineItems = new ReadOnlyCollection<LineItem>(lineItems);
  }
  public Customer Customer { get; }
  public IReadOnlyCollection<LineItem> LineItems { get; }
}
public class Customer {
  public Customer (string firstName, string lastName) {
    // my fingers are tired
  }
  public string FirstName { get; }
  public string LastName { get; }
  // etc
}
public class LineItem {
  public class LineItem(int quantity, decimal total, Product product) {
    // mommy are we there yet
  }
  public decimal Total { get; }
  public int Quantity { get; }
  public Product Product { get; }
}

Because C# doesn't have first-class support for an "immutable" type (like an F# Record Type), it takes a LOT of typing to build out these structures.

When you're done, you'll find that constructing these types is a huge pain. You'll get very long construction declarations:

var order = new Order(
   new Customer(
     "Bob",
     Saget"),
   new {
     new LineItem(10, 100m,
       new Product(
       )
   }
 );
     

Gross! How do we know what value corresponds to which property? We can add named arguments to make it readable, but those are optional, and still don't correspond to the actual property names (all camelCase). Typically you see no parameter names, making it quite difficult to understand how this all fits together.

Contrast this with a normal C# Object Initializer statement:

var order = new Order {
  Customer = new Customer {
    FirstName = "Bob",
    LastName = "Saget"
  },
  LineItems = new {
    new LineItem {
      Quantity = 10,
      Total = 100m,
      Product = new Product {
        
      }
    }
  }
};

I can see exactly how the entire object is built, one member at a time.

One other major issue with immutability in C# is the support for serializers and deserializers to work is quite a pain. If these tools see something without a setter, that's a problem. If we don't have a no-arg constructor, that's a problem. So we wind up having to bend the tools to be able to handle our types, and very often, with lots of compromises (private, unused setters etc.).

Finally, the nail in the coffin for me, is that when we introduce immutability via constructors, this introduces a breakable contract - a method with fixed arguments. If we add a value to our message, we introduce the possibility that receivers won't be able to properly deserialize.

Given all this, I find it's generally a net negative to attempt immutable DTOs. If the language, framework, and tooling supported it, that would be a different story.