3

Entities, Value Objects, and Aggregates

In the previous chapter, we learned about some of the core concepts of domain-driven design. In this chapter, we will build upon that foundational knowledge to learn more patterns and concepts, which will help you on your journey to mastering DDD. We will start by looking at entities and value objects. This is where we will write most of the business logic for our domain-driven application.

We will finish by looking at aggregates, which are useful when we need to cluster domain objects together and treat them as a single item.

In this chapter, we will cover the following topics:

  • What is an entity, and how should I use it?
  • What are some common pitfalls when designing entities, and how can I avoid them?
  • What is a value object, and how should I use it?
  • What is the aggregate pattern, and how should I use it?
  • How do I discover aggregates?

By the end of this chapter, you will be able to identify some common pitfalls while designing entities and how we can avoid them. You will also be able to tell what value objects and aggregate patterns are and to use them.

Let’s start by looking at entities.

Technical requirements

In this chapter, we will write a small amount of Golang code. To be able to run it, you will need the following:

Working with entities

In domain-driven design, entities are defined by their identity. Their attributes do not define them, and it is expected that although their attributes may change over time, their identity will not. While the entity may change so much that it is indistinguishable from where it started, it retains the same identity, and we treat it as the same object. Let’s look at an example. On ebay.com, you can sign up as a user. If you choose to sell something, you become a seller. You can also choose to bid on items. A naïve model of this might look as follows:

Figure 3.1 – A simple domain model for an auction site

Some actions that could take place in our system are as follows:

  • A user updates their address
  • A user updates their email address
  • An auction end time is updated

These actions do not change the identity of our entity. We are still referencing the same ID, but some attributes may have changed.

An implementation of the auction entity might look as follows:

package chapter3
import (
   "time"
   "github.com/Rhymond/go-money"
)
// Auction is an entity to represent our auction construct.
type Auction struct {
   ID int
   // We use a specific money library as floats are not good ways to represent money.
   startingPrice money.Money
   sellerID      int
   createdAt     time.Time
   auctionStart  time.Time
   auctionEnd    time.Time
}

As an exercise, see if you can write entities for bids and users.

In the preceding code sample, we have an int called ID – this is our entity ID. Entity IDs do not necessarily have to be generated by the system. They may form part of the entity’s attributes. For example, in most countries, people are issued a unique tax identification number that never changes. Therefore, it might be a good unique identifier for you to use if it’s relevant to your domain (for example, if you are building a HR system).

One interesting case is a user’s email address. At a glance, email addresses might seem like good entity identifiers as we will require them to be unique. However, in most systems, users can change the email address they will receive notifications on. Therefore, email addresses would be much better suited to be an attribute of the entity.

Generating good identifiers

Generating good unique identifiers for our entities is surprisingly hard. In the previous example, we used an int for our ID. For some good use cases, this will be fine as it is simple. However, in a system of substantial scale, we will quickly run into an issue.

Let’s say we write the following Go code:

fmt.Println(math.MaxInt)

We will get the following output:

9223372036854775807

However, let’s try and add 1 to this:

fmt.Println(math.MaxInt + 1)

We will get the following error:

cannot use math.MaxInt + 1 (untyped int constant 9223372036854775808) as int value in argument to fmt.Println (overflows)

We just ran out of integers! If we had used this as an identifier, we would now be experiencing a customer-facing production issue and would need to scramble to rearchitect our system under pressure effectively.

It is essential to try and future-proof your system as much as possible, especially when it comes to things such as entity identifiers. If you struggle to develop a good strategy for generating IDs, using universally unique identifiers (UUIDs) is a good place to start. UUIDs are 128-bit labels, which, when generated according to the specification, are effectively unique.

While UUIDs are not a part of the Go standard library, Google provides an excellent library for them. The following is an example of how to use UUIDs in Go:

package chapter3
import "github.com/google/uuid"
type SomeEntity struct {
   id uuid.UUID
}
func NewSomeEntity() *SomeEntity {
   id := uuid.New()
   return &SomeEntity{id: id}
}

If you are using a persistent store such as PostgreSQL, you can lean on it to create a UUID for you.

A warning when defining entities

Due to the focus of entities being on their identity, it is very easy to fall into the trap of letting the database design dictate what your domain model will look like. This can lead to what is known as an anemic domain model.

Anemic models

Anemic models have little or no domain behavior as part of their design. This means that you are not getting the full benefit of DDD. In my experience, entities are where anemia shows up most often. It is quite easy to diagnose anemic models and course-correct them if they’re identified early enough. If your model has mostly public getter and setter functions, no business logic, or depends on various clients to implement the business logic, you probably have an anemic model.

Here is what an anemic entity for our auction might look like:

package chapter3
import (
   "time"
   "github.com/Rhymond/go-money"
)
type AnemicAuction struct {
   id int
   startingPrice money.Money
   sellerID      int
   createdAt     time.Time
   auctionStart  time.Time
   auctionEnd    time.Time
}
func (a *AnemicAuction) GetID() int {
   return a.id
}
func (a *AnemicAuction) StartingPrice() money.Money {
   return a.startingPrice
}
func (a *AnemicAuction) SetStartingPrice(startingPrice money.Money) {
   a.startingPrice = startingPrice
}
func (a *AnemicAuction) GetSellerID() int {
   return a.sellerID
}
func (a *AnemicAuction) SetSellerID(sellerID int) {
   a.sellerID = sellerID
}
func (a *AnemicAuction) GetCreatedAt() time.Time {
   return a.createdAt
}
func (a *AnemicAuction) SetCreatedAt(createdAt time.Time) {
   a.createdAt = createdAt
}
func (a *AnemicAuction) GetAuctionStart() time.Time {
   return a.auctionStart
}
func (a *AnemicAuction) SetAuctionStart(auctionStart time.Time) {
   a.auctionStart = auctionStart
}
func (a *AnemicAuction) GetAuctionEnd() time.Time {
   return a.auctionEnd
}
func (a *AnemicAuction) SetAuctionEnd(auctionEnd time.Time) {
   a.auctionEnd = auctionEnd
}

Please note that there is nothing necessarily wrong with this, and you will see a lot of Go code that looks like this. But you simply are not getting the full benefit of the domain-driven design if you do this. As you can see, any other construct that uses our AnemicAuction will potentially make assumptions about what some of our attributes are for. Furthermore, they will implement business logic themselves and may do this in different ways that our domain experts did not intend.

Let’s refactor the code to the following:

package chapter3
import (
   "errors"
   "time"
   "github.com/Rhymond/go-money"
)
type AuctionRefactored struct {
   id            int
   startingPrice money.Money
   sellerID      int
   createdAt     time.Time
   auctionStart  time.Time
   auctionEnd    time.Time
}
func (a *AuctionRefactored) GetAuctionElapsedDuration() time.Duration {
   return a.auctionStart.Sub(a.auctionEnd)
}
func (a *AuctionRefactored) GetAuctionEndTimeInUTC() time.Time {
   return a.auctionEnd
}
func (a *AuctionRefactored) SetAuctionEnd(auctionEnd time.Time) error {
   if err := a.validateTimeZone(auctionEnd); err != nil {
      return err
   }
   a.auctionEnd = auctionEnd
   return nil
}
func (a *AuctionRefactored) GetAuctionStartTimeInUTC() time.Time {
   return a.auctionStart
}
func (a *AuctionRefactored) SetAuctionStartTimeInUTC(auctionStart time.Time) error {
   if err := a.validateTimeZone(auctionStart); err != nil {
      return err
   }
   // in reality, we would likely persist this to a database
   a.auctionStart = auctionStart
   return nil
}
func (a *AuctionRefactored) GetId() int {
   return a.id
}
func (a *AuctionRefactored) validateTimeZone(t time.Time) error {
   tz, _ := t.Zone()
   if tz != time.UTC.String() {
      return errors.New("time zone must be UTC")
   }
   return nil
}

Even in our simple example, we can see the benefit of our entity having some business logic. We have guaranteed that time zones are consistent, made it clear to the caller that we only deal with UTC, and enforced this with errors. We have also given them a consistent definition of the elapsed duration of our auction, rather than depending on the consumer to define it themselves, which could potentially lead to drift.

So, why can’t we use the same model for our database? As systems grow in complexity, you might find the need to store metadata about the auction, such as how many users viewed it, how effective ads were at pointing to this auction, or a tracing ID so that we can track a user’s journey through the system. All this information is useful, but it does not belong in the domain model.

A note on object-relational mapping

Object-relational mappings (ORMs) are a popular approach to managing database persistence. They are not a DDD concept, but they are popular enough that I thought it was worth a brief mention.

For Golang, GORM (https://gorm.io) is a popular library for this. I am not a fan of ORMs – they lead to a layer of unnecessary abstraction and poor database query design, which is often one of the biggest reasons applications have performance issues. By using an ORM, you are delegating control of query creation and planning.

If you want to use an ORM, ensure it does not control how you write your entities in your DDD context; otherwise, you may end up with an anemic model. We also want to keep the coupling between our entity and ORM to a minimum. Therefore, I recommend you use an adaptor layer to decouple your ORM and DDD entity layer. We covered adaptor layers in more detail in the previous chapter.

Now that we understand entities, let’s look at value objects.

Working with value objects

Value objects are, in some ways, the opposite of entities. With value objects, we want to assert that two objects are the same given their values. Value objects do not have identities and are often used in conjunction with entities and aggregates to enable us to build a rich model of our domain. We typically use them to measure, quantify, or describe something about our domain.

Before we go any further, let’s write some Golang code to help us understand value objects a bit further.

Firstly, we will define a Point in the following code block:

package chapter3
type Point struct {
   x int
   y int
}
func NewPoint(x, y int) *Point {
   return &Point{
      x: x,
      y: y,
   }
}

We will also write the following test, which checks if two points with the same coordinates are equal:

package chapter3_test
import (
   "testing"
   "ddd-golang/chapter3"
)
func Test_Point(t *testing.T) {
   a := chapter3.NewPoint(1, 1)
   b := chapter3.NewPoint(1, 1)
   if a != b {
      t.Fatal("a and  b were not equal")
   }
}

To a human, these two points are, of course, equal. You visit point A or point B on a map; you will end up in the same place – that is, at coordinates 1,1. However, this test fails:

=== RUN   Test_Point
    value_objects_test.go:13: a and  b were not equal
--- FAIL: Test_Point (0.00s)

So, why does it fail? In Golang, when we use the & symbol, we create a pointer to a memory address where points A and B are stored. When we do an equality check, they are not equal, as A and B are stored in different memory locations.

Now, let’s change our point definition to the following:

type Point struct {
   x int
   y int
}
func NewPoint(x, y int) Point {
   return Point{
      x: x,
      y: y,
   }
}

The test now passes (notice how we are no longer returning a pointer?). This is because the two points are now being compared on their values when we do an equality check. They are value objects; we can treat them equally if their values are equal.

Notice how, in the point class, x and y are lowercase? This is to stop them from being exported and mutated. It is recommended that value objects remain immutable to prevent any unexpected behavior.

Value objects should be replaceable. Imagine we are writing a game and using a point to represent the player’s current location. We might write some code to move our player, as follows:

package chapter3
type Point struct {
   x int
   y int
}
func NewPoint(x, y int) Point {
   return Point{
      x: x,
      y: y,
   }
}
const (
   directionUnknown = iota
   directionNorth
   directionSouth
   directionEast
   directionWest
)
func TrackPlayer() {
   currLocation := NewPoint(3, 4)
   currLocation = move(currLocation, directionNorth)
}
func move(currLocation Point, direction int) Point {
   switch direction {
   case directionNorth:
      return NewPoint(currLocation.x, currLocation.y+1)
   case directionSouth:
      return NewPoint(currLocation.x, currLocation.y-1)
   case directionEast:
      return NewPoint(currLocation.x+1, currLocation.y)
   case directionWest:
      return NewPoint(currLocation.x-1, currLocation.x)
   default:
      //do a barrel roll
   }
   return currLocation
}

The point here is a description of our player’s location. We can take advantage of the replaceability of the value object to update the point representing a player’s position to be a completely new value every time we move. In this specific instance, you’ll also notice that the move function is side effect free. This is something we should strive toward as part of immutability.

By following the principles of immutability and side-effect-free functions, we have made our value objects easier to reason about and to write unit tests for. We can write very simple tests with multiple different inputs with predictable outputs. This will help us with the long-term maintenance of the system.

How should I decide whether to use an entity or value object?

We should aim to use value objects as much as possible when modeling our domain. This is because they are the safest constructs we can use when implemented correctly. We do not have to worry about consumers incorrectly modifying our instance in a way we did not intend.

If you care only about the values of an object, then it should preferably be a value object. Some other questions to ask yourself to ensure a value object is the right choice for you are:

  • Is it possible for me to treat this object as immutable?
  • Does it measure, quantify, or describe a domain concept?
  • Can it be compared to other objects of the same type by its values?

If the answers to all these questions are yes, a value object is probably right for your use case.

At this point, it probably feels as if I am advising everything should be a value object. The truth is that this is not a bad way to think about it. Try and make everything a value object to start with until it does not fit your use case. At that point, it can be upgraded to an entity.

Now that we have a strong understanding of entities and value objects, we can build upon our knowledge and learn how to combine them with the aggregate pattern.

The aggregate pattern

Aggregates are probably one of the hardest patterns of domain-driven design and are, therefore, often implemented incorrectly. This isn’t necessarily bad if it helps you organize your code, but in the worst case, it may hinder your development speed and cause inconsistencies.

In domain-driven design, the aggregate pattern refers to a group of domain objects that can be treated as one for some behaviors. Some examples of aggregate patterns are:

  • An order: Typically, an order consists of individual items, but it is helpful to treat them as a single thing (an order) for some purposes within our system.
  • A team: A team consists of many employees. In our system, we would likely have a domain object for employees, but grouping them and applying behaviors to them as a team would be helpful in situations such as organizing departments.
  • A wallet: Typically, a wallet (even a virtual one) contains many cards and potential currencies for many countries and maybe even cryptocurrencies! We may want to track the value of the wallet over time and to do that, we may treat the wallet as an aggregate:

Figure 3.2 – An aggregate of a wallet holding a debit card, a credit card, and cryptocurrencies

Often, aggregates are confused with data structures used for collections of data, such as arrays, maps, and slices. These are not the same thing. While an aggregate may use these collections, an aggregate is a DDD concept and, therefore, will usually contain multiple collections, fields, functions, and methods. Instead, the job of an aggregate pattern is to act as a transaction boundary for the domain objects within. Loading, saving, editing, and deleting should happen to all objects within the aggregate or not at all. Let’s look at our examples again:

  • If an order is canceled, we should return all items within that order to stock. We may also want to trigger a refund.
  • If a new employee joins a team, we may need to update the line manager structure.
  • If a user adds a new card to their wallet, we need to ensure its balance is reflected in the total wallet balance.

Let’s look at how we might implement the wallet aggregate we mentioned here:

type WalletItem interface {
   GetBalance() (money.Money, error)
}
type Wallet struct {
   id uuid.UUID
   ownerID uuid.UUID
   walletItems []WalletItem
}
func (w Wallet) GetWalletBalance() (*money.Money, error) {
   var bal *money.Money
   for _, v := range w.walletItems {
      itemBal, err := v.GetBalance()
      if err != nil {
         return nil, errors.New("failed to get balance")
      }
      bal, err = bal.Add(&itemBal)
      if err != nil {
         return nil, errors.New("failed to increment balance")
      }
   }
   return bal, nil
}

Here are some interesting things to call out about this code block – id is our aggregate root and is our wallet’s identity. OwnerID is the identity of the entity that owns the wallet. We do not always need to know all the details of an owner, but it gives us the ability to fetch them when necessary. walletItems is a collection of WalletItem. WalletItem is an entity we defined elsewhere, so for now, we just define an interface.

Discovering aggregates

One of the hardest tasks of domain-driven design is trying to discover which type of construct to use and when. Before trying to cluster our domain models into aggregates, we need to find our bounded context’s invariants. An invariant is simply a rule in our domain that must always be true. For example, we may say that in our system, for an order to be created, we must have the item in stock. This is a business invariant. If we do not have an item in stock, we cannot promise it to customers.

For aggregates, we are looking for transactional consistency, not eventual consistency; we want any changes to our aggregate to be immediate and atomic. Therefore, we can think of an aggregate as a transactional consistency boundary. Whenever we make changes within our domain, we should ideally only modify one aggregate per transaction. If it is more, then your model is probably not quite correct, and you should revisit it.

Designing aggregates

Generally, we should aim for small aggregates. Keeping aggregates small will help make our system more scalable, improve performance, and give transactions more chance of success. Let’s look at the order system again and imagine a multi-user scenario (that is, multiple customers are trying to order the same item from a website at once). We could model our order aggregate as follows:

type item struct {
   name string
}
type Order struct {
   items          []item
   taxAmount      money.Money
   discount       money.Money
   paymentCardID  uuid.UUID
   customerID     uuid.UUID
   marketingOptIn bool
}

This order struct seems reasonable and in line with what we see in many order flows online today. However, including marketing opt-in in this aggregate is a bad design for a couple of reasons:

  • Firstly, from a bounded context perspective, marketing opt-in has nothing to do with the order object.
  • Secondly, if a user were to opt out of marketing between starting an order and completing it, we would not want the order to not complete. Therefore, removing it from our aggregate makes sense:
    type Order struct {
       items          []item
       taxAmount      money.Money
       discount       money.Money
       paymentCardID  uuid.UUID
       customerID     uuid.UUID
    }

Note

This does not mean we cannot include a marketing opt-in checkbox in our UI; it should just be decoupled from our aggregate and the transactional guarantee we want to achieve.

Aggregates beyond a single bounded context

Especially at the business scale, there will be situations where our bounded context changes and other sub-systems would like to be notified. Beyond our bounded context, we should expect (and aim for) eventual consistency. This means we expect the other systems to receive and process our event in a reasonable amount of time, but we do not expect it to be atomically up-to-date as we would expect our bounded contexts to be. This leads to more decoupled systems with stronger resilience and scalability possibilities. Check with the domain experts to see if eventual consistency is an acceptable trade-off to make room for these benefits. We will cover more about publishing domain events and microservices in Part 2 of this book.

Summary

In this chapter, we learned about entities, value objects, and aggregates. We saw why they can be challenging to reason about and why they are probably the most important building blocks of domain-driven design.

By now, we understand the difference between value objects and entities and why value objects are much safer to use generally. Furthermore, we have learned how to use aggregates to ensure transaction boundaries, which is important in any system!

In the next chapter, Chapter 4, Factories, Repositories, and Services, we will cover the final core concepts of domain-driven design before we build some more complex applications together in Part 2!

Further reading

Take a look at the following resources to learn more about the topics that were covered in this chapter:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.243.106