DEV Community

Cover image for Mastering DDD: Repository Design Patterns in Go
Yota Hamada
Yota Hamada

Posted on • Edited on • Originally published at Medium

Mastering DDD: Repository Design Patterns in Go

This article aims to deepen the understanding of "Repository", a core concept in Domain-Driven Design (DDD). We will explore the fundamental roles and importance of a repository and present an example of its implementation in the Go language.

Prerequisites

  • Assumes an application that retrieves (and updates) data from a relational database.
  • Sample code is written in Go.

What is a Repository?

Let's start by looking at the definition of a repository.

Repository Pattern:

A repository maps the data retrieved from the database into a structure and provides an interface to access domain objects.

This aligns with the general understanding of a repository. Next, let's look at a more detailed definition in the context of DDD.

A More Detailed Definition of Repository

Check out the DDD Reference. This reference is based on Eric Evans's book on DDD, summarizing various DDD terms. On page 17, it states:

Repository Definition (DDD Reference):

Query access to aggregates expressed in the ubiquitous language.

Proliferation of traversable associations used only for finding things muddles the model. In mature models, queries often express domain concepts. Yet queries can cause problems.
The sheer technical complexity of applying most database access infrastructure quickly swamps the client code, which leads developers to dumb-down the domain layer, which makes the model irrelevant.

A query framework may encapsulate most of that technical complexity, enabling developers to pull the exact data they need from the database in a more automated or declarative way, but that only solves part of the problem.

Unconstrained queries may pull specific fields from objects, breaching encapsulation, or instantiate a few specific objects from the interior of an aggregate, blindsiding the aggregate root and making it impossible for these objects to enforce the rules of the domain model. Domain logic moves into queries and application layer code, and the entities and value objects become mere data containers.

The Role of Repository

The role of a repository is to provide access to aggregates. The aspect of "executing SQL to fill a structure with data and return it" is just a superficial facet of a repository.

A repository always exists in tandem with an aggregate. Therefore, a correct understanding of aggregates is essential for practicing DDD.

What is an Aggregate?

Let's look into the term "aggregate". What is an aggregate? On page 16 of the DDD Reference, just above the repository section, there's an explanation of aggregates.

Definition of Aggregate (DDD Reference):

Cluster the entities and value objects into aggregates and define boundaries around each. Choose one entity to be the root of each aggregate, and allow external objects to hold references to the root only (references to internal members passed out for use within a single operation only). Define properties and invariants for the aggregate as a whole and give enforcement responsibility to the root or some designated framework mechanism.

Use the same aggregate boundaries to govern transactions and distribution.

Within an aggregate boundary, apply consistency rules synchronously. Across boundaries, handle updates asynchronously.

Keep an aggregate together on one server. Allow different aggregates to be distributed among nodes.

What is an Entity?

An entity is also defined in the DDD Reference. An entity is a domain object uniquely identifiable by an ID. In a domain model, there are entities and value objects (Value Object), the latter not having an ID.

Aggregate ≠ "Model"

The term "model" has been used in application development for a long time, but its meaning varies depending on who uses it and the context. In the past, in web application development, "model" meant the model in the MVC (Model-View-Controller) pattern.

Have you heard of Active Record? It's a design pattern adopted by Ruby on Rails and ORM libraries. In Active Record, a "model" holds data for one record of a table.

In Ruby on Rails, classes implementing Active Record are called "models" and are placed in the models/ path. Influenced by Rails, in application development, "model" usually refers to an object holding data for one row of a table.

Aggregate in DDD is different from Ruby on Rails's "model"

As mentioned, an aggregate in DDD can hold data not only for the root entity but also for other entities. Therefore, an aggregate is not a 1:1 relationship with a table but a 1:n relationship (n ≥ 1). An aggregate in DDD is not just for manipulating one record of a table.

An aggregate is a domain model. Even if composed of a single entity, please consider it a type of aggregate. A repository provides an interface to access aggregates.

Entities Composing an Aggregate

We know an aggregate comprises one or more entities. So, how should we decide which entities compose an aggregate?

The DDD Reference states:

An aggregate defines a unit that sets the attributes (properties) and invariants, responsible for maintaining these.

In other words, an aggregate is a unit that defines the attributes of a domain model and is responsible for maintaining the invariants demanded by that domain model.

Invariants are essentially relationships between data that must always be maintained. That's data consistency. To maintain data consistency (invariants), updates must occur within the same transaction. This forms the basis of an aggregate. In summary:

Entities Composing an Aggregate:
These are clusters of entities and value objects that should be treated as a unit in maintaining database data consistency (invariants). One of these entities becomes the root of the aggregate. Other aggregates can only hold references to the root entity, not the encapsulated entities within an aggregate.

Boundaries of an Aggregate

There's no silver bullet for correctly determining the boundaries of an aggregate. Properly setting aggregate boundaries requires consideration of database design, scalability, and domain knowledge. It's more challenging in complex domains, like inventory management systems.

Rule of Thumb:
Keep the boundaries of an aggregate as small as possible. This is to reduce the size of database transactions and, in turn, reduce the technical debt of the domain model.

Minimizing Aggregate Boundaries

How can we keep aggregate boundaries small? To do this, we need to consider it from the stage of table design.

The boundaries of an aggregate will become the boundaries for asynchronous processing when the service or product scales in the future. Therefore, you must decide boundaries such that data inconsistency doesn't occur even when transactions are split. Sometimes, tables need to be separated in unexpected ways.

As aggregates grow larger, the number of tables updated in synchronous transactions increases, leading to performance degradation and maintenance issues. Even if it seems that entities should be updated in one transaction, they might be separable upon closer examination. It's crucial not to be constrained by preconceptions.

Note:
When splitting transactions at aggregate boundaries, consider the implications of partial transaction failures. Ideally, a single operation in the user interface should not span multiple aggregate boundaries. Since aggregate boundaries are deeply intertwined with the user interface, it's important to align understanding with product managers and designers early in the process.

Example of a Shopping Cart

Let's consider the interface of a shopping cart for an e-commerce site. Assume there are tables for the shopping cart and cart items. Application layer use case X utilizes the data of the shopping cart and cart items (e.g., add a cart item into a shopping cart).

ER Diagram

ER diagram

Non-DDD Repository

The interface for a non-DDD approach would look like this, with a one-to-one correspondence between tables and repositories.

Class Diagram

Repository Interface:
Repositories are defined for the ShoppingCart and CartItem tables.

package repository

type ShoppingCart interface {
  GetByUserID(uuid.UUID) (*model.ShoppingCart, error)
  Insert(*model.ShoppingCart) error
  Update(*model.ShoppingCart) error
}

type CartItem interface {
  GetByShoppingCartID(id uuid.UUID) ([]*CartItem, error)
  Insert(*model.CartItem) error
  Update(*model.CartItem) error
}
Enter fullscreen mode Exit fullscreen mode

Model Definition:

Structures representing one record of each table are defined as models. Without interfaces, domain logic cannot be encapsulated

package model

type ShoppingCart struct {
  ID uuid.UUID
  UserID uuid.UUID
  Status model.ShoppingCartStatus
}

type CartItem struct {
  ID uuid.UUID
  ProductID string
  Quantity int
}
Enter fullscreen mode Exit fullscreen mode

Potential Issues:

This approach has the following potential issues:

  • Models become mere containers for table records.
  • Domain logic is implemented in use case X.
  • Aggregate invariants are ensured at the application layer.
  • Use case X needs to consider new and updated database operations.

DDD-Compliant Repository Example

Now, let's design a shopping cart repository following DDD principles.

The root of the aggregate is the ShoppingCart entity. CartItem entity is encapsulated within the ShoppingCart aggregate.

First, define a repository corresponding to the ShoppingCart aggregate. Unlike the previous example, there is no CartItem repository. This is because repositories are defined per aggregate, not per table. The ShoppingCart's AddItem method is an example of domain logic that is used by use case X.

Class Diagram:

Class Diagram

Repository Interface:

The repository encapsulates the process of reading and updating aggregates, so there's usually no need to separate Insert and Update. The repository takes responsibility for this in the Save method.

package domain

type ShoppingCartRepository interface {
  GetByUserID(uuid.UUID) (ShoppingCart, error)
  Save(ShoppingCart) error
}
Enter fullscreen mode Exit fullscreen mode

Aggregate Model Definition:

The root of the aggregate is the ShoppingCart entity. Aggregates encapsulate domain logic, so they are defined as interfaces, not structures, and are defined in the domain package. Here's the aggregate interface definition:

package domain

type ShoppingCart interface {
  ID() uuid.UUID
  AddItem(Product Product, Quantity int) error
}
Enter fullscreen mode Exit fullscreen mode

Improved Points:

  • The repository can fully encapsulate database complexity (insertion/update decisions).
  • No need to create a repository for each table, reducing redundant SQL coding.
  • By interfacing models, domain logic can be encapsulated.
  • Domain logic and database complexity are eliminated from the application layer.

Adopting DDD clarifies the responsibilities of domain logic, database access, and the application layer. This improvement reduces application complexity and cognitive load, enhancing development speed over time.

Repository and Aggregate Implementation

Should Aggregate and Repository Implementations be Separated?

Where should the implementations of the model and repository interfaces be placed? There's no definitive answer, but I personally believe it's preferable to place the implementations of repositories and models in the same package.

Reasons for Defining Repository and Aggregate Model Implementations in the Same Package:

The role of the repository was to encapsulate the complexity of database access, creation, and saving of aggregates. The repository needs to initialize attributes for instantiating the aggregate. Since it references and updates attributes not exposed in the interface, it needs to be defined in the same package.

For example, consider the Save method implementation of the repository. In the following implementation, the decision to register new or update in the database is encapsulated.

package shoppingcart

// Struct implementing domain.ShoppingCartRepository
type repositoryImpl struct {}

// Struct implementing domain.ShoppingCart
type shoppingCartImpl struct {
  ID uuid.UUID
  // other properties...
}

func (r *repositoryImpl) Save(model domain.ShoppingCart) error {
  instance := model.(*shoppingCartImpl)
  if instance.ID == uuid.Nil {
    // If ID is nil, it's a new registration
    return r.insert(instance)
  }
  return r.update(instance)
}
Enter fullscreen mode Exit fullscreen mode

Next, consider the GetByUserID method implementation of the repository. The repository can return an empty shopping cart if no data exists. Regardless of whether the aggregate exists, the right instance of the aggregate is returned, simplifying the repository interface.

package shoppingcart

func (r *repositoryImpl) GetByUserID(userID uuid.UUID) (domain.ShoppingCart, error) {
  data, err := r.findByUserID(userID)
  if errors.Is(err, sql.ErrNotFound) {
    // Return an empty shopping cart aggregate if no data exists
    return newEmptyShoppingCart(), nil
  }
  if err != nil {
    return nil, err
  }
  // Create an aggregate using the data of the already saved shopping cart
  return newShoppngCart(data), nil
}
Enter fullscreen mode Exit fullscreen mode

Deep Module: The Importance of a Simple Interface

Deep Module is a concept explained in the book A Philosophy of Software Design by a Stanford University professor. Deep Module refers to a module that has a simple and narrow interface on the surface but contains rich functionality and complexity internally. Conversely, modules with complex interfaces but little internal functionality are called Shallow Modules. Deep Modules excel in low cognitive load, high reusability, and ease of understanding. For example, Go language's net/http package is a simple interface but encapsulates many features and complexities for implementing an HTTP server, making it easy to use. The file system of an OS is another example of a Deep Module. The concept of Deep Module has gained wide support in the IT industry in recent years.

Depth Module

Source: Depth of module

Conclusion

Implementing repositories and aggregates according to DDD principles allows encapsulation of domain logic and enhances application maintainability. Determining aggregate boundaries requires deep consideration of domain and table design, but this is arguably one of the more enjoyable aspects of software development. In future developments, I aim to actively adopt DDD to explore more enjoyable application development.

PS: Wishing everyone in Ishikawa Prefecture, Noto, and Wajima in Japan, who were affected by the earthquake on New Year's Day, a safe recovery.

Top comments (1)

Collapse
 
lemon1997 profile image
lemon-1997

The article is very well written and very inspiring to me. Do you have any source code examples?