Skip to main content
  1. Articles/

Practical DDD in Golang: Repository

·2108 words·10 mins· loading · loading ·
Marko Milojevic
Author
Marko Milojevic
Software engineer and architect. Golang and LLM enthusiast. Awful chess player, gym rat, harmonica newbie and cat lover.
DDD in Golang - This article is part of a series.
Part 8: This Article

Today, it is hard to imagine writing an application without accessing some form of storage at runtime. This includes not only writing application code but also deployment scripts, which often need to access configuration files, which are also a type of storage in a sense. When developing applications to solve real-world business problems, connecting to databases, external APIs, caching systems, or other forms of storage is practically unavoidable. It’s no surprise, then, that Domain-Driven Design (DDD) includes patterns like the Repository pattern to address these needs. While DDD didn’t invent the Repository pattern, it added more clarity and context to its usage.

The Anti-Corruption Layer
#

Domain-Driven Design (DDD) is a principle that can be applied to various aspects of software development and in different parts of a software system. However, its primary focus is on the domain layer, which is where our core business logic resides. While the Repository pattern is responsible for handling technical details related to external data storage and doesn’t inherently belong to the business logic, there are situations where we need to access the Repository from within the domain layer.

Since the domain layer is typically isolated from other layers and doesn’t directly communicate with them, we define the Repository within the domain layer, but we define it as an interface. This interface serves as an abstraction that allows us to interact with external data storage without tightly coupling the domain layer to specific technical details or implementations.

A Simple Repository example

type Customer struct {
	ID uuid.UUID
	//
	// some fields
	//
}

type Customers []Customer

type CustomerRepository interface {
	GetCustomer(ctx context.Context, ID uuid.UUID) (*Customer, error)
	SearchCustomers(ctx context.Context, specification CustomerSpecification) (Customers, int, error)
	SaveCustomer(ctx context.Context, customer Customer) (*Customer, error)
	UpdateCustomer(ctx context.Context, customer Customer) (*Customer, error)
	DeleteCustomer(ctx context.Context, ID uuid.UUID) (*Customer, error)
}

The interface that defines method signatures within our domain layer is referred to as a “Contract.” In the example provided, we have a simple Contract interface that specifies CRUD (Create, Read, Update, Delete) methods. By defining the Repository as this interface, we can use it throughout the domain layer. The Repository interface always expects and returns our Entities, such as Customer and Customers (collections with specific methods attached to them, as defined in Go).

It’s important to note that the Entity Customer doesn’t contain any information about the underlying storage type, such as Go tags for defining JSON structures, Gorm columns, or anything of that sort. This kind of low-level storage configuration is typically handled in the infrastructure layer.

The Domain Layer

type CustomerRepository interface {
	GetCustomer(ctx context.Context, ID uuid.UUID) (*Customer, error)
	SearchCustomers(ctx context.Context, specification CustomerSpecification) (Customers, int, error)
	SaveCustomer(ctx context.Context, customer Customer) (*Customer, error)
	UpdateCustomer(ctx context.Context, customer Customer) (*Customer, error)
	DeleteCustomer(ctx context.Context, ID uuid.UUID) (*Customer, error)
}

DAO on the Infrastructure Layer

type CustomerGorm struct {
	ID   uint   `gorm:"primaryKey;column:id"`
	UUID string `gorm:"uniqueIndex;column:uuid"`
	//
	// some fields
	//
}

func (c CustomerGorm) ToEntity() (model.Customer, error) {
	parsed, err := uuid.Parse(c.UUID)
	if err != nil {
		return Customer{}, err
	}
	
	return model.Customer{
		ID: parsed,
		//
		// some fields
		//
	}, nil
}

Repository on the Infrastructure Layer

type CustomerRepository struct {
	connection *gorm.DB
}

func (r *CustomerRepository) GetCustomer(ctx context.Context, ID uuid.UUID) (*model.Customer, error) {
	var row CustomerGorm
	err := r.connection.WithContext(ctx).Where("uuid = ?", ID).First(&row).Error
	if err != nil {
		return nil, err
	}
	
	customer, err := row.ToEntity()
	if err != nil {
		return nil, err
	}
	
	return &customer, nil
}
//
// other methods
//

In the example above, you can observe a snippet of CustomerRepository implementation. Internally, it utilizes Gorm for smoother integration, but you can also use pure SQL queries if preferred. Lately, I’ve been using the Ent library extensively. In this example, you encounter two distinct structures: Customer and CustomerGorm.

The first structure serves as an Entity, intended for housing our business logic, domain invariants, and rules. It remains oblivious to the underlying database. The second structure functions as a Data Access Objects (DAO, responsible solely for mapping data to and from the storage system. This structure doesn’t have any other role aside from facilitating the mapping of database data to our Entity.

The separation of these two structures is a fundamental aspect of using the Repository pattern as an Anti-Corruption layer in our application. It ensures that technical details related to table structure don’t contaminate our business logic. What are the implications of this approach? Firstly, it necessitates the management of two types of structures: one for business logic and one for storage. Additionally, a third structure is often introduced, which serves as a Data Transfer Objects (DTO for our API. This approach introduces complexity into our application and entails the creation of multiple mapping functions, as exemplified in the code snippet below. It’s essential to thoroughly test such methods to prevent common copy-paste errors.

Entities on the Domain Layer

type Customer struct {
	ID      uuid.UUID
	Person  *Person
	Company *Company
	Address Address
}

type Person struct {
	SSN       string
	FirstName string
	LastName  string
	Birthday  Birthday
}

type Birthday time.Time

type Company struct {
	Name               string
	RegistrationNumber string
	RegistrationDate   time.Time
}

type Address struct {
	Street   string
	Number   string
	Postcode string
	City     string
}

DAOs on the Infrastructure Layer

type CustomerGorm struct {
	ID        uint         `gorm:"primaryKey;column:id"`
	UUID      string       `gorm:"uniqueIndex;column:id"`
	PersonID  uint         `gorm:"column:person_id"`
	Person    *PersonGorm  `gorm:"foreignKey:PersonID"`
	CompanyID uint         `gorm:"column:company_id"`
	Company   *CompanyGorm `gorm:"foreignKey:CompanyID"`
	Street    string       `gorm:"column:street"`
	Number    string       `gorm:"column:number"`
	Postcode  string       `gorm:"column:postcode"`
	City      string       `gorm:"column:city"`
}

func (c CustomerGorm) ToEntity() (model.Customer, error) {
	parsed, err := uuid.Parse(c.UUID)
	if err != nil {
		return model.Customer{}, err
	}

	return model.Customer{
		ID:      parsed,
		Person:  c.Person.ToEntity(),
		Company: c.Company.ToEntity(),
		Address: Address{
			Street:   c.Street,
			Number:   c.Number,
			Postcode: c.Postcode,
			City:     c.City,
		},
	}, nil
}

type PersonGorm struct {
	ID        uint      `gorm:"primaryKey;column:id"`
	SSN       string    `gorm:"uniqueIndex;column:ssn"`
	FirstName string    `gorm:"column:first_name"`
	LastName  string    `gorm:"column:last_name"`
	Birthday  time.Time `gorm:"column:birthday"`
}

func (p *PersonGorm) ToEntity() *model.Person {
	if p == nil {
		return nil
	}

	return &model.Person{
		SSN:       p.SSN,
		FirstName: p.FirstName,
		LastName:  p.LastName,
		Birthday:  Birthday(p.Birthday),
	}
}

type CompanyGorm struct {
	ID                 uint      `gorm:"primaryKey;column:id"`
	Name               string    `gorm:"column:name"`
	RegistrationNumber string    `gorm:"column:registration_number"`
	RegistrationDate   time.Time `gorm:"column:registration_date"`
}

func (c *CompanyGorm) ToEntity() *model.Company {
	if c == nil {
		return nil
	}

	return &model.Company{
		Name:               c.Name,
		RegistrationNumber: c.RegistrationNumber,
		RegistrationDate:   c.RegistrationDate,
	}
}

func NewRow(customer model.Customer) CustomerGorm {
	var person *PersonGorm
	if customer.Person != nil {
		person = &PersonGorm{
			SSN:       customer.Person.SSN,
			FirstName: customer.Person.FirstName,
			LastName:  customer.Person.LastName,
			Birthday:  time.Time(customer.Person.Birthday),
		}
	}

	var company *CompanyGorm
	if customer.Company != nil {
		company = &CompanyGorm{
			Name:               customer.Company.Name,
			RegistrationNumber: customer.Company.RegistrationNumber,
			RegistrationDate:   customer.Company.RegistrationDate,
		}
	}

	return CustomerGorm{
		UUID:     uuid.NewString(),
		Person:   person,
		Company:  company,
		Street:   customer.Address.Street,
		Number:   customer.Address.Number,
		Postcode: customer.Address.Postcode,
		City:     customer.Address.City,
	}
}

However, despite the additional maintenance involved, this approach adds significant value to our codebase. It allows us to represent our Entities within the domain layer in a manner that best encapsulates our business logic. We are not restricted by the storage solution we employ. For instance, we can use one type of identifier within our business logic (such as UUID) and a different one for the database (unsigned integer). This flexibility extends to any data we wish to use for the database and business logic.

When modifications are made in either of these layers, it is likely that we will need to make corresponding adjustments in mapping functions, while the rest of the layer remains untouched (or at least minimally impacted). We can opt to switch to a different database system like MongoDB or Cassandra, or even switch to an external API, all without affecting our domain layer.

Persistence
#

The Repository primarily serves for querying purposes and integrates seamlessly with another DDD pattern known as Specification, as you may have observed in the examples. While it can be used without Specification, it often simplifies our workflow. The second key function of the Repository is Persistence. It encompasses the logic for persisting our data in the underlying storage, ensuring its permanence, facilitating updates, and even enabling deletion when necessary.

Generate UUID

type CustomerRepository struct {
	connection *gorm.DB
}

func (r *CustomerRepository) SaveCustomer(ctx context.Context, customer Customer) (*Customer, error) {
	row := NewRow(customer)
	err := r.connection.WithContext(ctx).Save(&row).Error
	if err != nil {
		return nil, err
	}

	customer, err = row.ToEntity()
	if err != nil {
		return nil, err
	}

	return &customer, nil
}

In some scenarios, we opt for generating unique identifiers within an application. In such cases, the Repository is the appropriate location for this task. In the provided example, you can observe that we generate a new UUID before creating the database record. We can employ a similar approach with integers if we aim to avoid relying on auto-incrementing database keys. Regardless of the method chosen, when we prefer not to depend on database-generated keys, it is advisable to create identifiers within the Repository.

Database Transactions

type CustomerRepository struct {
	connection *gorm.DB
}

func (r *CustomerRepository) CreateCustomer(ctx context.Context, customer Customer) (*Customer, error) {
	tx := r.connection.Begin()
	defer func() {
		if r := recover(); r != nil {
			tx.Rollback()
		}
	}()

	if err := tx.Error; err != nil {
		return nil, err
	}

	//
	// some code
	//

	var total int64
	var err error
	if customer.Person != nil {
		err = tx.Model(PersonGorm{}).Where("ssn = ?", customer.Person.SSN).Count(&total).Error
	} else if customer.Person != nil {
		err = tx.Model(CompanyGorm{}).Where("registration_number = ?", customer.Person.SSN).Count(&total).Error
	}
	if err != nil {
		tx.Rollback()
		return nil, err
	} else if total > 0 {
		tx.Rollback()
		return nil, errors.New("there is already such record in DB")
	}
	
	//
	// some code
	//
	
	err = tx.Save(&row).Error
	if err != nil {
		tx.Rollback()
		return nil, err
	}

	err = tx.Commit().Error
	if err != nil {
		tx.Rollback()
		return nil, err
	}

	customer := row.ToEntity()

	return &customer, nil
}

Another important function of the Repository is managing transactions. When we need to persist data and perform multiple queries that operate on the same extensive set of tables, it is a suitable situation to establish a transaction, which should be managed within the Repository.

In the provided example, we are verifying the uniqueness of a Person or Company. If they already exist, we return an error. All of these operations can be defined as part of a single transaction, and if any part of it fails, we can roll it back. In this context, the Repository serves as an ideal location for such code. It’s worth noting that, in the future, we might simplify our inserts to the extent that transactions are no longer required. In that case, we won’t need to change the Repository’s contract, only the internal code.

Types of Repositories
#

It is a mistake to think that we should use the Repository pattern exclusively for databases. While we frequently use it with databases since they are the primary choice for storage, alternative storage options have gained popularity today. As previously mentioned, we can utilize MongoDB or Cassandra as alternatives. Repositories can also be employed to manage our cache, where Redis, for instance, would be a suitable choice. Repositories can even be applied to REST APIs or configuration files when necessary.

Redis Repository

type CustomerRepository struct {
	client *redis.Client
}

func (r *CustomerRepository) GetCustomer(ctx context.Context, ID uuid.UUID) (*Customer, error) {
	data, err := r.client.Get(ctx, fmt.Sprintf("user-%s", ID.String())).Result()
	if err != nil {
		return nil, err
	}

	var row CustomerJSON
	err = json.Unmarshal([]byte(data), &row)
	if err != nil {
		return nil, err
	}
	
	customer := row.ToEntity()

	return &customer, nil
}

REST API Repository

type CustomerRepository struct {
	client *http.Client
	baseUrl string
}

func (r *CustomerRepository) GetCustomer(ctx context.Context, ID uuid.UUID) (*Customer, error) {
	resp, err := r.client.Get(path.Join(r.baseUrl, "users", ID.String()))
	if err != nil {
		return nil, err
	}
	
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var row CustomerJSON
	err = json.Unmarshal(data, &row)
	if err != nil {
		return nil, err
	}

	customer := row.ToEntity()

	return &customer, nil
}

Now we can truly appreciate the advantage of separating our business logic from technical details. By maintaining the same interface for our Repository, our domain layer remains unchanged. However, as our application expands, we may find that MySQL is no longer the ideal solution for our distributed application. In the event of a migration, we can transition without concern for how it will impact our business logic, as long as we maintain consistent interfaces.

Therefore, your Repository Contract should always revolve around your business logic, while your Repository implementation can use internal structures that can later be mapped to Entities.

Conclusion
#

The Repository is a well-established pattern responsible for querying and persisting data in the underlying storage. It serves as the primary point for Anti-Corruption within our application. We define it as a Contract within the domain layer and house the actual implementation within the infrastructure layer. It is where we generate application-specific identifiers and manage transactions.

Useful Resources
#

DDD in Golang - This article is part of a series.
Part 8: This Article

Related

Practical DDD in Golang: Factory

·1023 words·5 mins· loading · loading
When I wrote the title of this article, I was trying to remember the first design pattern I had learned from “The Gang of Four”. I think it was one of the following: Factory Method, Singleton, or Decorator. I am sure that other software engineers have a similar story. When they started learning design patterns, either Factory Method or Abstract Factory was one of the first three they encountered. Today, any derivative of the Factory pattern is essential in Domain-Driven Design, and its purpose remains the same, even after many decades. Complex Creations # We use the Factory pattern for any complex object creation or to isolate the creation process from other business logic. Having a dedicated place in the code for such scenarios makes it much easier to test separately. In most cases, when I provide a Factory, it is part of the domain layer, allowing me to use it throughout the application. Below, you can see a simple example of a Factory. Simple example type Loan struct { ID uuid.UUID // // some fields // } type LoanFactory interface { CreateShortTermLoan(specification LoanSpecification) Loan CreateLongTermLoan(specification LoanSpecification) Loan } The Factory pattern goes hand-in-hand with the Specification pattern. Here, we have a small example with LoanFactory, LoanSpecification, and Loan. LoanFactory represents the Factory pattern in DDD, and more specifically, the Factory Method. It is responsible for creating and returning new instances of Loan that can vary depending on the payment period. Variations # As mentioned, we can implement the Factory pattern in many different ways. The most common form, at least for me, is the Factory Method. In this case, we provide some creational methods to our Factory struct. Loan Entity const ( LongTerm = iota ShortTerm ) type Loan struct { ID uuid.UUID Type int BankAccountID uuid.UUID Amount Money RequiredLifeInsurance bool } Loan Factory type LoanFactory struct{} func (f *LoanFactory) CreateShortTermLoan(bankAccountID uuid.UUID, amount Money) Loan { return Loan{ Type: ShortTerm, BankAccountID: bankAccountID, Amount: amount, } } func (f *LoanFactory) CreateLongTermLoan(bankAccountID uuid.UUID, amount Money) Loan { return Loan{ Type: LongTerm, BankAccountID: bankAccountID, Amount: amount, RequiredLifeInsurance: true, } } In the code snippet from above, LoanFactory is now a concrete implementation of the Factory Method. It provides two methods for creating instances of the Loan Entity. In this case, we create the same object, but it can have differences depending on whether the loan is long-term or short-term. The distinctions between the two cases can be even more complex, and each additional complexity is a new reason for the existence of this pattern.

Practical DDD in Golang: Aggregate

·2269 words·11 mins· loading · loading
I have spent years understanding and practicing the DDD approach. Most of the principles were easy to understand and implement in the code. However, there was one that particularly caught my attention. I must say that the Aggregate pattern is the most critical one in DDD, and perhaps the entire Tactical Domain-Driven Design doesn’t make sense without it. It serves to bind business logic together. While reading, you might think that the Aggregate resembles a cluster of patterns, but that is a misconception. The Aggregate is the central point of the domain layer. Without it, there is no reason to use DDD. Business Invariants # In the real business world, some rules are flexible. For example, when you take a loan from a bank, you need to pay some interest over time. The overall amount of interest is adjustable and depends on your invested capital and the period you will spend to pay the debt. In some cases, the bank may grant you a grace period, offer you a better overall credit deal due to your loyalty in the past, provide you with a once-in-a-lifetime offer, or require you to place a mortgage on a house. All of these flexible rules from the business world are implemented in DDD using the Policy pattern. They depend on many specific cases and, as a result, require more complex code structures. In the real business world, there are also some immutable rules. Regardless of what we try, we cannot change these rules or their application in our business. These rules are known as Business Invariants. For example, nobody should be allowed to delete a customer account in a bank if any of the bank accounts associated with the customer has money or is in debt. In many banks, one customer may have multiple bank accounts with the same currency. However, in some of them, the customer is not allowed to have any foreign currency accounts or multiple accounts with the same currency. When such business rules exist, they become Business Invariants. They are present from the moment we create the object until the moment we delete it. Breaking them means breaking the whole purpose of the application. Currency Entity type Currency struct { id uuid.UUID // // some fields // } func (c Currency) Equal(other Currency) bool { return c.id == other.id } BankAccount Entity

Practical DDD in Golang: Module

·2203 words·11 mins· loading · loading
At first glance, Modules may not seem like a typical software development pattern, especially when we often associate patterns with specific code structures or behaviors. This can be particularly confusing when considering Go Modules. These modules consist of closely related Go Packages, are versioned, and released together, serving as a form of dependency management in Go. Since both Go Modules and Packages impact the project’s structure, it raises the question of their relationship with the DDD pattern known as Module. Indeed, there is a connection between them. The Structure # In Go, we use Packages to organize and group our code. Packages are closely tied to the folder structure within our projects, although there can be variations in naming. These variations arise because we have the flexibility to name our package differently than the actual folder it resides in. Folder pkg/access/domain/model package access_model import ( "github.com/google/uuid" ) type User struct { ID uuid.UUID // // some fields // } Folder pkg/access/domain/service package access_service import ( "project/pkg/access/domain/model" ) type UserService interface { Create(user access_model.User) error // // some methods // } In the example above, you can observe slight differences between folder and package naming. In some cases, when dealing with multiple model packages, I add prefixes from my DDD Modules to facilitate referencing these packages within the same file. Now, we can start to gain a better understanding of what a DDD Module would be in the previous example. In this context, the Module encompasses the access package along with all its child packages. project ├── cmd │ ├── main.go ├── internal │ ├── module1 │ │ ├── infrastructure │ │ ├── presentation │ │ ├── application │ │ ├── domain │ │ │ ├── service │ │ │ ├── factory │ │ │ ├── repository │ │ │ └── model │ │ └── module1.go │ ├── module2 │ │ └── ... │ └── ... ├── pkg │ ├── module3 │ │ └── ... │ ├── module4 │ │ └── ... │ └── ... ├── go.mod └── ... The folder structure in the diagram above represents my preferred project structure for implementing Domain-Driven Design in Go. While I may create different variations of certain folders, I strive to maintain consistent DDD Modules. In my projects, each Module typically consists of no more than four base packages: infrastructure, presentation, application, and domain. As you can see, I adhere to the principles of Hexagonal Architecture. In this structure, I place the infrastructure package at the top. This is because, by following Uncle Bob’s Dependency Inversion Principle, my low-level Services from the infrastructure layer implement high-level interfaces from other layers. With this approach, I ensure that I define a Port, such as the UserRepository interface, in the domain layer, while the actual implementation resides in the infrastructure layer. There can be multiple Adapters for this implementation, like UserDBRepository or UserFakeRepository.