Skip to content

Leaking Value Objects from your Domain

Sponsor: Using RabbitMQ or Azure Service Bus in your .NET systems? Well, you could just use their SDKs and roll your own serialization, routing, outbox, retries, and telemetry. I mean, seriously, how hard could it be?

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.


Value Objects are a great way to explicitly capture concepts within your domain. They are immutable, always in a valid state, provide behavior, and are defined by their value. This sounds a lot like Messages (Commands, Events) that are also immutable and should be in a valid state. However, exposing your Value Objects by using them within Commands or Events can have a negative impact on your ability to evolve your domain model.

YouTube

Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything that is in this post.

Value Objects

First, let’s cover what Value objects are since they have many characteristics that define them. They are explicit domain concepts that should always be in a valid state. They are immutable once created, which means they are always created in a valid state. Since they cannot be mutated they also have the benefit of being defined by their value. Lastly, they should have behavior, which is a characteristic that is often overlooked.

A typical example of a Value Object is Money. Money isn’t just a decimal. Especially in a multi-currency environment. The combination of the amount and the currency is required to make it valid.

using Xunit;
namespace ValueObject
{
public record Currency(string Symbol)
{
public static Currency CAD => new("CAD");
public static Currency USD => new("USD");
}
public record Money
{
public Currency Currency { get; }
public decimal Amount { get; }
public Money(Currency currency, decimal amount)
{
Currency = currency;
Amount = amount;
}
}
public class MoneyTests
{
[Fact]
public void Test()
{
var money1 = new Money(Currency.CAD, 100);
var money2 = new Money(Currency.CAD, 100);
Assert.Equal(money1, money2);
}
}
}
view raw Money.cs hosted with ❤ by GitHub

Another typical example is Distance. Again, the distance isn’t simply a number, but rather can be a number along with a unit of measure.

using System;
using Xunit;
namespace ValueObject
{
public abstract record UnitOfMeasure;
public record Kilometers : UnitOfMeasure;
public record Miles : UnitOfMeasure;
public record Distance
{
public UnitOfMeasure UnitOfMeasure { get; }
public decimal Value { get; }
public Distance(UnitOfMeasure unitOfMeasure, decimal value)
{
UnitOfMeasure = unitOfMeasure;
Value = value;
}
public Distance ToMiles()
{
switch (UnitOfMeasure)
{
case Miles:
return new Distance(new Miles(), Value);
case Kilometers:
return new Distance(new Miles(), Math.Round(Value * 0.621371m, 2, MidpointRounding.AwayFromZero));
default:
throw new InvalidOperationException();
}
}
public Distance ToKilometers()
{
switch (UnitOfMeasure)
{
case Miles:
return new Distance(new Kilometers(), Math.Round(Value * 1.60934m, 2, MidpointRounding.AwayFromZero));
case Kilometers:
return new Distance(new Kilometers(), Value);
default:
throw new InvalidOperationException();
}
}
}
public class DistanceTest
{
[Fact]
public void Test()
{
var distance1 = new Distance(new Kilometers(), 100);
var distance2 = distance1.ToMiles();
var distance3 = distance2.ToKilometers();
Assert.Equal(distance3, distance1);
}
}
}
view raw Distance.cs hosted with ❤ by GitHub

Messaging

Commands and Events in a Message or Event Driven Architecture look very similar to Value Objects. Messages are explicit domain concepts that are immutable and in a valid state. So can you have Value Objects inside a Command or Event?

using System;
using ValueObject.Command;
namespace ValueObject
{
public record PlaceOrderCommand(Guid CustomerId, Product Product, int Quantity, Currency Currency);
}
view raw PlaceOrder.cs hosted with ❤ by GitHub

In the example above, the PlaceOrderCommand has two Value Objects: Product and Currency. These are explicit concepts we’ve defined within a boundary.

Since messages are for the purpose of decoupling between boundaries, this means that other boundaries must be aware of these as concepts.

Putting Value Objects in your messages means you’re going to be leaking details outside of your service boundary. The consequences of this are that since messages are contracts, you’ll need to think about versioning any time you want to change a Value Object since it’s a part of a Message.

Rather, you want to keep domain concepts from leaking outside of your service boundary. Concepts within your service boundary you want to be able to refactor, change, rename, remove without having to concern yourself with other services. The purpose of messaging is decoupling and using messages as a stable contract. The moment you leak something like a Value Object into a message, you’ve coupled other services to concepts within your service boundary.

Conversion

Instead of leaking Value Objects, you can create Messages/DTOs that may look similar. Simply have some type of conversion that accepts your Value Objects as parameters but have the message being built be simple primitives or nested types.

using System;
using ValueObject;
namespace ValueObject2
{
public record PlaceOrderCommand(Guid CustomerId, string ProductSku, int Quantity, string CurrencySymbol)
{
public PlaceOrderCommand(Guid customerId, string productSku)
: this(customerId, productSku, 1, Currency.USD.ToString()) { }
public PlaceOrderCommand(Guid customerId, string productSku, string currency)
: this(customerId, productSku, 1, currency) { }
public PlaceOrderCommand(Guid customerId, string productSku, int quantity)
: this(customerId, productSku, quantity, Currency.USD.ToString()) { }
}
}
view raw PlaceOrder2.cs hosted with ❤ by GitHub

As another example, that’s using a nested type.

using System;
using ValueObject;
namespace ValueObject3
{
public record PlaceOrderCommand(Guid CustomerId, string ProductSku, int Quantity, PlaceOrderCurrency Currency)
{
public PlaceOrderCommand(Guid customerId, string productSku)
: this(customerId, productSku, 1, PlaceOrderCurrency.Default) { }
public PlaceOrderCommand(Guid customerId, string productSku, Currency currency)
: this(customerId, productSku, 1, new PlaceOrderCurrency(currency.Symbol)) { }
public PlaceOrderCommand(Guid customerId, string productSku, int quantity)
: this(customerId, productSku, quantity, PlaceOrderCurrency.Default) { }
}
public record PlaceOrderCurrency(string Symbol)
{
public static PlaceOrderCurrency Default => new("USD");
};
}
view raw PlaceOrder3.cs hosted with ❤ by GitHub

Don’t Leak Value Objects

They are as much a domain concept as Entities are. If you’re not going to expose Entities then do not expose Value Objects. Although they may seem trivial, you’ll be handcuffed into versioning your messages if you do need to change a them in any way.

Messages are contracts for other services. You want to message contracts to have some stability. Although Value Objects have similar characteristics as messages (Commands and Events), they are meant to be internal while Messages are meant for other services boundaries.

Source Code

Developer-level members of my CodeOpinion YouTube channel get access to the full source for any working demo application that I post on my blog or YouTube. Check out the membership for more info.

Related Links

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.