Practical DDD in Golang: Value Object
- 9 minutes read - 1885 wordsLet's begin a practical journey into Domain-Driven Design in Golang, starting with the most important pattern: Value Objects.

Saying that a particular pattern is the most important might seem like an exaggeration, but I wouldn’t even argue against it. The first time I encountered the concept of a Value Object was in Martin Fowler’s book. At that time, it seemed quite simple and not very interesting. The next time I read about it was in Eric Evans’ “The Big Blue Book.” At that point, the pattern started to make more and more sense, and soon enough, I couldn’t imagine writing my code without incorporating Value Objects extensively.
Simple but beautiful
At first glance, a Value Object seems like a simple pattern. It gathers a few attributes into one unit, and this unit performs certain tasks. This unit represents a particular quality or quantity that exists in the real world and associates it with a more complex object. It provides distinct values or characteristics. It could be something like a color or money (which is a type of Value Object), a phone number, or any other small object that offers value, as shown in the code block below.
Quantity
type Money struct {
Value float64
Currency Currency
}
func (m Money) ToHTML() string {
returs fmt.Sprintf(`%.2f%s`, m.Value, m.Currency.HTML)
}
Quality
type Color struct {
Red byte
Green byte
Blue byte
}
func (c Color) ToCSS() string {
return fmt.Sprintf(`rgb(%d, %d, %d)`, c.Red, c.Green, c.Blue)
}
Type extension
type Salutation string
func (s Salutation) IsPerson() bool {
returs s != "company"
}
Logical Group
type Phone struct {
CountryPrefix string
AreaCode string
Number string
}
func (p Phone) FullNumber() string {
returs fmt.Sprintf("%s %s %s", p.CountryPrefix, p.AreaCode, p.Number)
}
In Golang, you can depict Value Objects by creating new structs or by enhancing certain basic types. In either scenario, the goal is to introduce specialized functionalities for that individual value or a set of values. Frequently, Value Objects can supply particular methods for formatting strings to determine how values should operate during JSON encoding or decoding. However, the primary purpose of these methods should be to maintain the business rules linked to that particular characteristic or quality in real life.
Identity and Equality
A Value Object lacks identity, and that’s its key distinction from the Entity pattern. The Entity pattern possesses an identity that distinguishes its uniqueness. If two Entities share the same identity, it implies they refer to the same objects. On the other hand, a Value Object lacks such identity. It only consists of fields that provide a more precise description of its value. To determine equality between two Value Objects, we must compare the equality of all their fields, as demonstrated in the code block below.
Color Value Object
func (c Color) EqualTo(other Color) bool {
return c.Red == other.Red && c.Green == other.Green && c.Blue == other.Blue
}
Money Value Object
func (m Money) EqualTo(other Money) bool {
return m.Value == other.Value && m.Currency.EqualTo(other.Currency)
}
Currency Entity
func (c Currency) EqualTo(other Currency) bool {
return c.ID.String() == other.ID.String()
}
In the example above, both the Money
and Color
structs have defined EqualTo
methods
that examine all their fields. However, Currency
checks for equality based on their
Identities, which in this example are UUIDs.
As you can see, a Value Object can also reference an Entity object, as is the case
with Money
and Currency
here. It can also include smaller Value Objects, like the
Coin
struct, which comprises both Color
and Money
. Alternatively, it can define a
slice to store a collection of Colors
.
Additional Value Objects
type Coin struct {
Value Money
Color Color
}
type Colors []Color
In one Bounded Context,
we may have numerous Value Objects. However, some of them may
actually serve as Entities within other Bounded Contexts. This is the case for Currency
.
In a basic Web Service
scenario where we simply want to display money, we can treat
Currency
as a Value Object, tightly linked to our Money
object, which we don’t intend
to modify. On the other hand, in a Payment Service
where we require real-time updates
through an Exchange Service API
, we need to use identities within the Domain Model.
In this situation, we’ll have distinct implementations of Currency
in different services.
Web Service
type Currency struct {
Code string
HTML int
}
Payment Service
type Currency struct {
ID uuid.UUID
Code string
HTML int
}
The choice of whether to use the Value Object or Entity pattern solely depends on what the object signifies within the Bounded Context. If it’s an object that can be reused, stored independently in the database, can undergo changes and be applied to multiple other objects, or is linked to an external Entity that must change whenever the external one changes, we refer to it as an Entity. However, if an object represents a value, is associated with a specific Entity, is essentially a direct copy from an external service, or should not exist independently in the database, then it qualifies as a Value Object.
Explicitness
The most valuable aspect of a Value Object is its clarity. It offers transparency to the
outside world, especially in situations where the default types in Golang (or any other
programming language) do not support specific behavior or where the supported behavior is
not intuitive. For example, when dealing with a customer across various projects that need
to adhere to certain business rules, such as being an adult or representing a legal entity,
using more explicit types like Birthday
and LegalForm
is a valid approach.
Birthday Value Object
type Birthday time.Time
func (b Birthday) IsYoungerThen(other time.Time) bool {
return time.Time(b).After(other)
}
func (b Birthday) IsAdult() bool {
return time.Time(b).AddDate(18, 0, 0).Before(time.Now())
}
LegalForm Value Object
const (
Freelancer = iota
Partnership
LLC
Corporation
)
type LegalForm int
func (s LegalForm) IsIndividual() bool {
return s == Freelancer
}
func (s LegalForm) HasLimitedResponsability() bool {
return s == LLC || s == Corporation
}
Sometimes, a Value Object doesn’t necessarily have to be explicitly designated as
part of another Entity or Value Object. Instead, we can define a Value Object as a
helper object that enhances clarity for future use in the code. This situation arises
when dealing with a Customer
who can either be a Person
or a Company
. Depending on
the Customer
’s type, the application follows different pathways. One of the more
effective approaches could involve transforming customers to simplify handling them.
Value Objects
type Person struct {
FullName string
Birthday Birthday
}
type Company struct {
Name string
CreationDate time.Time
}
Customer Entity
type Customer struct {
ID uuid.UUID
Name string
LegalForm LegalForm
Date time.Time
}
func (c Customer) ToPerson() Person {
return Person{
FullName: c.Name,
Birthday: c.Date,
}
}
func (c Customer) ToCompany() Company {
return Company{
Name: c.Name,
CreationDate: c.Date,
}
}
While cases involving transformations may occur in some projects, in the majority of situations, they indicate that we should include these Value Objects as an integral part of our Domain Model. In fact, when we observe that a specific subset of fields consistently interact with each other, even though they are part of a larger group, it’s a clear signal that we should group them into a Value Object. This allows us to use them as a single unit within our larger group, effectively making the larger group smaller in scope.
Immutability
Value Objects are immutable. There is no single cause, reason, or argument to alter the state of a Value Object throughout its existence. Occasionally, multiple objects may share the same Value Object (though this isn’t a perfect solution). In such cases, we certainly don’t want to modify Value Objects in unexpected locations. Therefore, whenever we intend to modify an internal state of a Value Object or combine multiple of them, we must always return a new instance with the updated state, as shown in the code block below.
A Wrong Approach
func (m *Money) AddAmount(amount float64) {
m.Amount += amount
}
func (m *Money) Deduct(other Money) {
m.Amount -= other.Amount
}
func (c *Color) KeppOnlyGreen() {
c.Red = 0
c.Bed = 0
}
The Right Approach
func (m Money) WithAmount(amount float64) Money {
return Money {
Amount: m.Amount + amount,
Currency: m.Currency,
}
}
func (m Money) DeductedWith(other Money) Money {
return Money {
Amount: m.Amount - other.Amount,
Currency: m.Currency,
}
}
func (c Color) WithOnlyGreen() Color {
return Color {
Red: 0,
Green: c.Green,
Blue: 0,
}
}
In all examples, the correct approach is to consistently return new instances and leave the old ones unchanged. In Golang, it’s a best practice to associate functions with values rather than references to Value Objects, ensuring that we never modify their internal state.
The Right Approach with Validation
func (m Money) Deduct(other Money) (*Money, error) {
if !m.Currency.EqualTo(other.Currency) {
return nil, errors.New("currencies must be identical")
}
if other.Amount > m.Amount {
return nil, errors.New("there is not enough amount to deduct")
}
return &Money {
Amount: m.Amount - other.Amount,
Currency: m.Currency,
}, nil
}
This immutability implies that we shouldn’t validate a Value Object throughout its entire existence. Instead, we should validate it only during its creation, as demonstrated in the example above. When creating a new Value Object, we should always carry out validation and return errors if business invariants are not met. If the Value Object passes validation, we can create it. After that point, there is no need to validate the Value Object anymore.
Rich behavior
A Value Object offers a variety of different behaviors. Its primary role is to furnish a rich interface. If it lacks methods, we should question its purpose and whether it truly serves any meaningful role. When a Value Object does make sense within a specific part of the code, it brings a substantial set of additional business invariants that more effectively describe the problem we aim to address.
Color Value Object
func (c Color) ToBrighter() Color {
return Color {
Red: math.Min(255, c.Red + 10),
Green: math.Min(255, c.Green + 10),
Blue: math.Min(255, c.Blue + 10),
}
}
func (c Color) ToDarker() Color {
return Color {
Red: math.Max(0, c.Red - 10),
Green: math.Max(0, c.Green - 10),
Blue: math.Max(0, c.Blue - 10),
}
}
func (c Color) Combine(other Color) Color {
return Color {
Red: math.Min(255, c.Red + other.Red),
Green: math.Min(255, c.Green + other.Green),
Blue: math.Min(255, c.Blue + other.Blue),
}
}
func (c Color) IsRed() bool {
return c.Red == 255 && c.Green == 0 && c.Blue == 0
}
func (c Color) IsYellow() bool {
return c.Red == 255 && c.Green == 255 && c.Blue == 0
}
func (c Color) IsMagenta() bool {
return c.Red == 255 && c.Green == 0 && c.Blue == 255
}
func (c Color) ToCSS() string {
return fmt.Sprintf(`rgb(%d, %d, %d)`, c.Red, c.Green, c.Blue)
}
Breaking down the entire Domain Model into smaller components like Value Objects (and Entities) clarifies the code and aligns it with real-world business logic. Each Value Object can represent specific components and facilitate various functions similar to standard business processes. Ultimately, this simplifies the entire unit testing process and aids in addressing all possible scenarios.
Conclusion
The real world is full of various characteristics, qualities, and quantities. Since software applications aim to address real-world issues, the use of such descriptors is unavoidable. Value Objects are introduced as a solution to tackle this need for clarity in our business logic.