I started my software development career as a structural engineer who programmed, developing analytical applications for other engineers. When I moved to a traditional software company, I went from one extreme to another, from having all the roles of a software development team at once to becoming a "code monkey" with no context about what I was building and why.
In search of a middle ground I came across Domain-Driven Design (DDD), which in its essence is about getting to know the Domain you are developing software for. But without being a Domain expert yourself.
For my next project I started my DDD journey by implementing what is known as tactical Domain-Driven Design. These are patterns applied at the code level. There is also Strategic Domain-Driven Design which describes patterns that can be applied on a higher, architectural level.
I quickly started seeing advantages of applying tactical patterns that went beyond the separation of concerns, that an isolated Domain Layer with a model containing data and behavior (Rich Domain Model) brings. The pattern that stood out to me was Value Objects. It is relatively straightforward to grasp and implement and still allow you to practice the philosophy of DDD.
In this post I will show which features of the Kotlin Language I find especially useful to implement Value Objects (including Domain Primitives) while developing the SwatchIt Android application.
A swatch is a small sample of fabric knitted before starting a project. Because every knitterās tension and materials vary, swatches allow you to measure your stitch and row count (gauge). This ensures your finished piece will be the correct size.
About Value Objects
All data objects in a Domain-Driven Design Model are either Entities or Value Objects. Value Objects are objects without an identity that are primarily defined by their attributes. Entities are defined primarily by their identity, because we need to keep track of them over their whole lifecycle, even if all their attributes change. In the context of SwatchIt a swatch is an entity. The attributes, like the yarn or the notes, might be changed by the user, but we still see it as the same swatch. There could be two swatches with the same attributes in our system, but we see them as conceptually different swatches. So we cannot use the attributes to track the swatch. We need something that makes the swatch unique and identifiable in our system: an identity usually implemented as an Int, or a UUID.
In real life all physical objects have an identity. They might change, but we usually still see them as the same objects. The aim of modelling is to make things analyzable and calculable. To achieve that aim we usually have to strip everything from reality that is not relevant to our analysis.
Modelling means distilling reality into a concept that is exactly as complex as it absolutely needs to be for the task at hand. The model needs to strike a balance between being too naive to make any useful assumptions and being too complex to be analyzable and calculable. Distinguishing Value Objects and Entities is one tool to achieve that.
If you are not interested in following an object over its whole lifecycle, you model this object as a Value Object, without an identity. If the user changes the knitting needle size, it is not important which exact pair of knitting needles the user had in mind. All that is important in the context of SwatchIt is the size of the needles. The knitting needle object is defined by it's attributes (the size) and not by an identity.
There are no general rules. What is considered a Value Object and what an Entity can change from context to context and every team decides for themselves what should be modelled as what.
Not requiring an identity has several advantages:
- You can share instances, which saves memory on the machine.
- Value Objects are immutable as per their definition. Meaning that an instance's attributes are set during construction and cannot be changed after that.
Leveraging immutability has several advantages:
- It makes the code easier to reason about -> No need to take sideeffects into account that could occur because the instance changes.
- No defensive copying -> You can safely pass instances around and other objects can reference the same instance without risk that one object could change a shared instance.
- Better thread safety -> One thread cannot change an instance that another thread is using as well, if the instance is immutable.
- Testing is also more straight forward, because it is much easier to think of and cover all possible states.
Implementation in Kotlin
When implementing Value Objects you can make use of features that your chosen language and framework offer you. Here comes an overview over what I found useful in Kotlin/JVM. Most of the examples are taken directly from the code of the SwatchIt Android application.
Data classes
When you compare Value Objects they are considered equal when they have the same attributes. You can use two knitting needle objects interchangeably as long as they have the same size value. To reflect that in the code you would override the Equals and HashCode methods to reflect that.
Instead of doing that manually for each Value Object you can leverage Kotlin's Data class. It automatically implements the Equals and HashCode methods based on the attributes of the class, which is exactly what we want.
It is also quite easy to create immutable data classes by defining all properties in the primary constructor as readonly with the val keyword:
data class Yarn(
val name: YarnName?,
val manufacturer: YarnManufacturer?
)
If a property should not be considered for comparing it can be defined in the class body:
data class GaugeSize(val value: Double) {
val unit: LengthUnit = LengthUnit.CM
...
}
// Equality will be determined by the value only,
// the unit property is not taken into account
Additionally data classes come with some useful standard methods implemented, for example a copy method that makes handling immutable objects easier and reduces boilerplate:
val newYarn =
oldYarn.copy(name = Name("new yarn name"))
// creates a new yarn instance with the same manufacturer value
// as oldYarn, but a different name
The automatic toString method of a Yarn instance generates: "Yarn(name="new yarn name", manufacturer = "manufacturer")", which is helpful for debugging.
As someone being familiar with C# I see resemblances between Records in C# and data classes in Kotlin. One difference I noticed is that you cannot have a private constructor in data classes. So forcing the creation of instances via a factory method is not possible. However you must use the Primary Constructor. Validation can be added to an init block like this:
data class Yarn(
val name: YarnName?,
val manufacturer: YarnManufacturer?
) {
init {
require(name != null || manufacturer != null) {
"Yarn must have at least a name or manufacturer"
}
}
...
}
The init block is run when the Primary Constructor is executed. This behavior makes sure that the validation logic in the init block is always run, even if an object is created via the copy method. This can be somewhat of a pitfall in C#, unless you are careful, the with expression (which is C# equivalent to copy) can bypass constructor-based validation, leading to invalid domain states. Also with clones the whole object and then sets the properties one by one. The init block makes validation of Cross-Field Invariants easier, as it ensures the object is only fully initialized if the entire state passes validation in one atomic operation.
Another difference is that Kotlin's data classes implement structural equality by default even for collection properties (be careful with Arrays though).
data class Yarn(val skeins: List<Skein>)
data class Skein(val yardage: Long, val color: String)
class Swatch(val skeins: List<Skein>)
fun main() {
val blueSkein = Skein(200, "blue")
val redSkein = Skein(220, "red")
val skeins1 = listOf(blueSkein, redSkein)
val skeins2 = listOf(blueSkein, redSkein)
// structural equality for collections
println(skeins1 == skeins2) // true
val yarn1 = Yarn(listOf(blueSkein, redSkein))
val yarn2 = Yarn(listOf(blueSkein, redSkein))
// structural equality in data classes
println(yarn1 == yarn2) // true
val swatch1 = Swatch(listOf(blueSkein, redSkein))
val swatch2 = Swatch(listOf(blueSkein, redSkein))
// referential equality in classes
println(swatch1 == swatch2) // false
}
Furthermore, it is possible to inherit from Records in C#. Records use an EqualityContract property to make sure that you are comparing the same type (and not a parent with a child for example). That makes overriding the Equals and HashCode methods of records a little tricky as you need to remember to add a check EqualityContract == other.EqualityContract to maintain type-safety. So not only do you have to override those methods as soon as you use a collection property, you also have to remember the EqualityContract.
In Kotlin all classes are by default closed for inheritance (final). To open a class for inheritance you need to add the open keyword. But Data classes can never be open. If you need inheritance you typically use a sealed interface or a regular abstract class instead.
Interfaces in Kotlin can be sealed, which means that only classes within the same module can inherit from the interface. All the inheritors are therefore known at compile-time. This is nice because it gives you full control over your interfaces in the Domain Layer and makes it clear that this interface should only be extended within the context of this module.
As an example SwatchIt uses a sealed interface to distinguish between two types of measurements (rows and stitches) like this:
sealed interface Measurement {
val count: GaugeCount
val size: GaugeSize
data class Rows(
override val count: GaugeCount,
override val size: GaugeSize
) : Measurement
data class Stitches(
override val count: GaugeCount,
override val size: GaugeSize
) : Measurement
}
As all possible inheritors are known at compile time, there's no need to add a default value in when expression blocks, when checking the type of the Measurement instance, because the compiler enforces exhaustiveness:
private fun Measurement.toListItem(): MeasurementListItem = when (this) {
is Measurement.Stitches -> MeasurementListItem.Stitches(
count = count.value,
size = size.value
)
is Measurement.Rows -> MeasurementListItem.Rows(
count = count.value,
size = size.value
)
}
Again: less boilerplate, yay!
Inline value classes
Inline value classes are meant for wrapping underlying types (like Long, Int, String..). They give you the possibility to use more domain specific types without increasing the runtime overhead (in most cases). This is perfect for wrapping these simple values into small Value Objects (also called Domain Primitives). You get type safety, immutability and validation without any performance overhead.
Inline value classes are defined by the keyword value, and they need the @JvmInline attribute if your code is supposed to be run on the JVM:
@JvmInline
value class YarnName private constructor(val value: String)
To ensure they can be "unwrapped" into a single value, these classes are limited. They must have exactly one primary constructor parameter and no other backing fields. However, you can still add computed properties and member functions. These are compiled to static Java methods, meaning you can add logic to your domain primitives without breaking the performance optimization.
You might want to override the toString() method of your inline value classes. By default, the Kotlin compiler generates a toString() that includes the class name, like YarnName(value="My Yarn Name"). If you want your domain primitive to behave exactly like its underlying type when printed (e.g., just printing My Yarn Name), you should override it:
override fun toString(): String = value
Looking at the decompiled bytecode, we can see that this override doesn't "break" the optimization. The compiler simply replaces its generated static method with our custom static method:
@NotNull
public static String toString_impl/* $FF was: toString-impl*/(String var0) {
return var0;
}
As with data classes, Equals and HashCode methods do not need to be overwritten, they use the underlying type for comparison.
Unlike in data classes, you can use private primary constructors in value classes and thus force the creation via factory methods in Companion Objects:
@JvmInline
value class YarnName private constructor(val value: String) {
init {
require(validate(value) is ValidationResult.Success) {
"YarnName invalid"
}
}
companion object {
private const val MAX = 50
fun validate(value: String): ValidationResult {
return when {
value.length > MAX ->
ValidationResult.Error("max 50")
else ->
ValidationResult.Success
}
}
fun create(value: String): YarnName? =
if (value.isBlank()) null else YarnName(value)
}
override fun toString(): String = value
}
I'll get back to the ValidationResult interface in a minute.
Using factory methods to create objects is another widely used concept in Domain-Driven Design. Sometimes the creation of an object is complicated and you want this logic to stay in within the class. You just expose a convenient creation method (aka factory method) that speaks its intent. My example is, as you can see, pretty minimal, but I hope you get the concept.
Companion Objects
In Kotlin, class level functions and properties have to be implemented within so called companion objects. The functions and properties within the companion objects can be called the same way static functions and properties are called in other languages. I just wanted to mention them here, because it took me while to get used to them. And as you need static functions to implement factory methods and static validation here is an example of how you could implement both with a Companion object:
companion object {
private const val MAX = 50
fun validate(value: String): ValidationResult {
return when {
value.length > MAX ->
ValidationResult.Error("max 50")
else ->
ValidationResult.Success
}
}
fun create(value: String): YarnName? =
if (value.isBlank()) null else YarnName(value)
}
The ValidationResult implementation looks like this (note the sealed interface in action again):
sealed interface ValidationResult {
data object Success : ValidationResult
data class Error(
val errorMessageId: Int
) : ValidationResult
}
It wraps the result of a validation, so instead of just returning a bool, it can return an error message id, that can be used to show the specific validation error to the user.
Another option is to throw an exception. A failing validation is in this case expected, as the method is used to validate user input.
If however the application tries to create an instance that is not valid, that would be unexpected, because at this point the input should have been validated. Something must have gone wrong and the application throws an exception.
This has no direct connection to Domain-Driven Design though, it is just how I like to handle validation at the moment. However validation plays a big role in Domain-Driven Design.
Validation
Making sure your application never ends up in an invalid state is the responsibility of the domain layer. Knowing what constitutes a valid or an invalid state is domain knowledge. Writing all those checks produces lots and lots of code and can be tedious. Kotlin is here to help!
The build-in require function shrinks validation code to a minimum:
require(validate(value).isValid) { "Pattern invalid" }
It throws an InvalidArgumentException, which is pretty generic. You might want to throw custom exceptions for you and others using your code to be able to react to them in a more fine grained manner.
Similar to require there is also the check function which works in the same way as the require function but throws an IllegalStateException.
Extension methods
Value Objects often need small utility functions for formatting, conversion, or calculation. Rather than adding every helper function as a class member, Kotlin's extension methods let you keep related functionality close to where it's used while maintaining clean, readable Value Object definitions.
Kotlin does not require you to wrap functions in classes, they can be declared at the top level of a file. That reduces the boilerplate code otherwise needed to write extension methods. Here's an example of an extension method:
private fun Double.roundToOneDecimalPlace(): Double {
val multiplier = 10.0
return round(this * multiplier) / multiplier
}
this references the value that the function is called upon in this case.
Classes defined within the same file as the function can then call that function (it is marked private):
fun calculateWidthFor(stitches: Count): Size {
val result = (stitches.value * size.value / nrOfStitches.value)
.roundToOneDecimalPlace()
return Size(result)
}
This makes it clear that this extension function is only valid within the file/class/package it is placed in and it is better discoverable by The IDE than passing the double value as a parameter.
Operator overloading
Operator overloading can be convenient for Value Objects, especially for Domain Primitives that only wrap a primitive type. Instead of for example adding two Counts by calling their values (count1.value + count2.value) the + operator can be overloaded like this:
@JvmInline
value class Count(val value: Long) {
operator fun plus(summand: Count): Count {
return Count(value + summand.value)
}
}
// Tests:
@Test
fun add_validSummands_returnsSumOfValues() {
val sum = Count(13) + Count(14)
assertEquals(Count(27), sum)
}
Operator overloading should of course not be overused or misused. It must be obvious for the developer calling the overload what should happen.
Conclusion
And that concludes my list of suggestions. If you haven't tried Kotlin I can recommend the Playground where you can run code in your browser. From the books on Kotlin I have read so far I liked Kotlin in Action best.
I found Kotlin to be very convenient to implement Value Objects. Implementing them is boilerplate-prone and can seem as over-engineering, especially for people who haven't seen the advantages yet. Kotlin does a lot of the heavy lifting and invalidates those arguments, it lets you write concise code while giving you the advantages of leveraging Value Objects.
If you have remarks, ideas I should consider or just feel like discussing, please reach out. I am by no means set in my ways and super interested in your experiences and opinions.
