Domain Driven Design in Go: Entities

Dennis Vis
mei 8 2023
9 min read
Where Value Objects represent pieces of immutable data without an identity, entities represent identifiable elements within our systems. Entities generally have a lifecycle. They can be created, oftentimes deleted and are expected to be mutated during their lifespan.

This post is part of the series "Domain Driven Design in Go".

In this series, I explain how I apply Domain Driven Design (DDD) in Go projects. This is not meant to be a definitive guide on how this should be done. Instead, I want to show you how it can be done. These are the methods I have come to apply frequently, after trying out many different ways, and they work quite well for me. But I’m still fine-tuning these methods frequently.

Entities are regarded as equal if their identities are equal. This means that, even if two entities contain different data, as long as their identifying field’s value is the same, they must be regarded as the same entity. In that situation, it’s likely that one of the entities is an updated version of the other.

In this post I’ll discuss how entities can be implemented in Go.

Use a struct

There is not a lot to say regarding this first point. Entities in Go should be implemented as structs. This is the only type within the language that can be used towards this end. Entities need to have fields. At least one, the identifying field. And this can only be achieved using a struct.

Use a Value Object as identifier

Use a suitable Value Object as identifier. Using a Value Object allows for being more expressive and adding any needed domain logic to its type, like generation, validation or equality checks.

You can choose to use one of the many UUID libraries available, as this can save you a lot of logic that you otherwise have to implement and thoroughly test yourself. Just make sure to prevent tight coupling of your domain logic to such a library. You want to keep the door open for potentially swapping the library with another. At a minimum, specify your own UUID type, derived from the library’s UUID type. This way, the choice of library can be opaque to the rest of your domain layer, as there you’ll want to only reference the derived type. For example:

package uuid

import googleuuid "github.com/google/uuid"

type UUID googleuuid.UUID

Just be aware that swapping the backing library will not be a straightforward feat. If there are already entities in the wild, which use the previous implementation, you’ll need to properly support both the previous and the new ID type at the same time. So take your time to investigate and choose the library wisely. You should preferably not have to change it for the lifetime of the code base.

Another option is to leverage a product within your technical landscape to create an ID for you. For instance; an automatically incrementing integer, provided by a database. I would generally try to avoid this, as it couples your domain layer to a specific technology.

Note that an ID does not always have to be a UUID or auto-incrementing integer. For example: let’s say we have a User entity. A user will have an email address. That email address uniquely identifies a user, so we might use it as an identifier, like so:

package user

import "email"

// User represents a person interacting with our system.
type User struct {
    emailAddress email.Address
}

// Equal returns true if a [User] is considered equal to the given [User].
func (u *User) Equal(x *User) bool {
    return u.emailAddress == x.emailAddress
}

Don’t blindly copy this example! Be aware of the privacy concerns this introduces first. An email address is regarded as PII . A randomly generated ID is much easier to deal with when it comes to privacy. For instance: when using a random ID, if a user wants their data deleted, we can delete or obfuscate all PII of the user entity. Things like audit trails can still show the user’s behaviour but we won’t be able to relate it back to a specific person anymore. If you use an email address as the ID, you would need to delete all references to it. This not only disrupts things like audit trails, it also requires a lot more orchestration and complexity.

Passing by reference

Since an entity refers to a single, unique element, it should generally be passed around as a pointer to said element. An instance is created once, kept in memory and, whenever it’s referenced, we always point to the same location in memory.

Doing so will mean any mutations will be done on the same instance. This should limit the chance of multiple versions of the same entity existing within an application.

If an entity is to be accessed and/or mutated by different Go routines, you’ll need to make sure you properly design for this concurrency. Towards this end, you’ll likely want to follow the common Go advice of sharing memory by communicating.

There can be a variety of reasons for not passing an entity by reference, performance being one, and this can be acceptable. Just be aware that it will take more careful planning to make sure the logic stays sound in that case. You will have to make sure that you continuously overwrite local copies of the entity with the mutated copies that are sent back from other parts of the application.

Struct fields

The fields of an entity, struct fields in Go, can be made up of Value Objects or other entities. At least one field should be present; the identifying field using a proper Value Object type. The other fields compose the data which the entity encapsulates.

Since we’re passing around pointers to entities, the entities can be mutated from anywhere where these pointers are passed to. Often this is desirable when, for instance, we want an entity’s state to change, depending on some business logic. But allowing direct mutation of an entity’s fields breaks encapsulation and can potentially make the code more difficult to reason about and/or debug.

So, unless you have a very good reason not to, you should probably make all fields of an entity unexported.

But then how can clients read or mutate the current state of an entity? By using good old getters and setters. Many developers are of the mind that these are not idiomatic Go. But in reality, using them is perfectly fine. The naming convention is a bit different than in other languages, though. As getters in Go should not be prefixed with Get. For instance: a field id would be exposed through a method called ID().

This does not mean, however, that you should provide a getter and setter for every field. Or that every setter should blindly accept any given value. These methods become the public interface for the entity. And, as is the case with any public interface, careful thought needs to be put into its design.

For example: the identifier field of an entity should probably be exposed through a getter. But we certainly do not want to allow for this field to be mutated in any way. So we leave out the setter. As an other example; let’s say we have an entity which contains a display name. We might not want to accept just any display name. Maybe we want to make sure it’s of a maximum size or that it does not contain any profanities. The SetDisplayName method would be a good location for this, as it keeps the logic close to the data.

Expressing domain logic

As a general rule of thumb: any business logic that involves the fields of a single entity, and does not require external dependencies, should be added to a method of that particular entity. For example: if we need to limit the amount of characters within the displayName field of a User entity, we should add a check for it in the SetDisplayName like so:

package user

import (
    "email"
    "errors"
    "fmt"
)

const displayNameMaxLen = 256

// ErrDisplayNameTooLong is returned when a [DisplayName] exceeds
// the maximum allowed length.
var ErrDisplayNameTooLong = errors.New(
    fmt.Sprintf(
        "display name contains more than %d characters",
        displayNameMaxLen,
    ),
)

// DisplayName is a name used in specific presentation situations.
type DisplayName string

// Validate checks if the [DisplayName] contains a valid value.
func (dn DisplayName) Validate() error {
    if len(dn) > displayNameMaxLen {
        return ErrDisplayNameTooLong
    }

    return nil
}

// User represents a person interacting with our system.
type User struct {
    emailAddress email.Address
    displayName  DisplayName
}

// SetDisplayName sets the [DisplayName] of a [User] to the given value
// if the given value is valid. It will return an error if the given
// value is not a valid [DisplayName].
func (u *User) SetDisplayName(displayName DisplayName) error {
    err := displayName.Validate()
    if err != nil {
        return fmt.Errorf("user: set display name: %w", err)
    }

    u.displayName = displayName

    return nil
}

Notice how the display name validation is delegated to the DisplayName Value Object. This improves separation of concerns and keeps the code more DRY. For this example, the impact is obviously not that big. But there will likely be other entities which can share the same functionality.

If there’s other business logic needed, which involves external dependencies, or other entities which are not a field of the target entity, it should live separately from the entity. For example: say we’re using a third party, external system to detect profanities in the display name, we don’t want to pollute the entity with the code needed to leverage this system. Entities should only be concerned with the data they represent and the behaviour directly derived from that data.

So where do we place this logic? That will be discussed in a future post about services.

A Word On Aggregates

In DDD, entities are always used in the context of aggregates. You cannot design your system effectively without considering which entities make up which aggregates.

Even though “Expressing domain logic”, as it’s described in this post, is correct, it should really be done from the perspective of an aggregate. So, before you start adding your entities and their methods, make sure you understand aggregates first.

Conclusion

Entities can be properly implemented in Go by:

  • Using a struct to represent them
  • Adding an identifying struct field which uses a suitable Value Object as type
  • Passing them around as a pointer to a single instance in memory
  • Not exporting the struct fields but supplying explicit methods to get and/or set their values
  • Adding struct methods which represent domain logic related to an entity’s data and nothing else

Entities are a very important part of the overall domain design. They are a direct, digital representation of elements that make up the business. Take care to put enough thought and consideration into the design of their fields and public interface.