Practical DDD in Golang: Value Object

- 9 minutes read - 1885 words

Let'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.

Useful Resources

comments powered by Disqus