Practical DDD in Golang: Factory

- 5 minutes read - 1018 words

The story about Domain-Driven Design (DDD) continues by introducing a legendary pattern: the Factory.

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.

Investment interface and implementations

type Investment interface {
	Amount() Money
}

type EtfInvestment struct {
	ID             uuid.UUID
	EtfID          uuid.UUID
	InvestedAmount Money
	BankAccountID  uuid.UUID
}

func (e EtfInvestment) Amount() Money {
	return e.InvestedAmount
}

type StockInvestment struct {
	ID             uuid.UUID
	CompanyID      uuid.UUID
	InvestedAmount Money
	BankAccountID  uuid.UUID
}

func (s StockInvestment) Amount() Money {
	return s.InvestedAmount
}

Investment Factories

type InvestmentSpecification interface {
	Amount() Money
	BankAccountID() uuid.UUID
	TargetID() uuid.UUID
}

type InvestmentFactory interface {
	Create(specification InvestmentSpecification) Investment
}

type EtfInvestmentFactory struct{}

func (f *EtfInvestmentFactory) Create(specification InvestmentSpecification) Investment {
	return EtfInvestment{
		EtfID:          specification.TargetID(),
		InvestedAmount: specification.Amount(),
		BankAccountID:  specification.BankAccountID(),
	}
}

type StockInvestmentFactory struct{}

func (f *StockInvestmentFactory) Create(specification InvestmentSpecification) Investment {
	return StockInvestment{
		CompanyID:      specification.TargetID(),
		InvestedAmount: specification.Amount(),
		BankAccountID:  specification.BankAccountID(),
	}
}

In the example above, there is a code snippet with the Abstract Factory pattern. In this case, we want to create instances of the Investment interface. Since there are multiple implementations of that interface, this seems like a perfect scenario for implementing the Factory pattern. Both EtfInvestmentFactory and StockInvestmentFactory create instances of the Investment interface. In our code, we can keep them in a map of InvestmentFactory interfaces and use them whenever we want to create an Investment from any BankAccount. This is an excellent use case for the Abstract Factory pattern, as we need to create objects from a wide range of possibilities (and there are even more different types of investments).

Reconstruction

We can also use the Factory pattern in other layers, such as the infrastructure and presentation layers. In these layers, I use it to transform Data Transfer Objects (DTO or Data Access Objects (DAO to Entities and vice versa.

The Domain Layer

type CryptoInvestment struct {
	ID               uuid.UUID
	CryptoCurrencyID uuid.UUID
	InvestedAmount   Money
	BankAccountID    uuid.UUID
}

DAO on the Infrastructure Layer

type CryptoInvestmentGorm struct {
	ID                 int                `gorm:"primaryKey;column:id"`
	UUID               string             `gorm:"column:uuid"`
	CryptoCurrencyID   int                `gorm:"column:crypto_currency_id"`
	CryptoCurrency     CryptoCurrencyGorm `gorm:"foreignKey:CryptoCurrencyID"`
	InvestedAmount     int                `gorm:"column:amount"`
	InvestedCurrencyID int                `gorm:"column:currency_id"`
	Currency           CurrencyGorm       `gorm:"foreignKey:InvestedCurrencyID"`
	BankAccountID      int                `gorm:"column:bank_account_id"`
	BankAccount        BankAccountGorm    `gorm:"foreignKey:BankAccountID"`
}

Factory on the Infrastructure Layer

type CryptoInvestmentDBFactory struct{}

func (f *CryptoInvestmentDBFactory) ToEntity(dto CryptoInvestmentGorm) (model.CryptoInvestment, error) {
	id, err := uuid.Parse(dto.UUID)
	if err != nil {
		return model.CryptoInvestment{}, err
	}

	cryptoId, err := uuid.Parse(dto.CryptoCurrency.UUID)
	if err != nil {
		return model.CryptoInvestment{}, err
	}

	currencyId, err := uuid.Parse(dto.Currency.UUID)
	if err != nil {
		return model.CryptoInvestment{}, err
	}

	accountId, err := uuid.Parse(dto.BankAccount.UUID)
	if err != nil {
		return model.CryptoInvestment{}, err
	}

	return model.CryptoInvestment{
		ID:               id,
		CryptoCurrencyID: cryptoId,
		InvestedAmount:   model.NewMoney(dto.InvestedAmount, currencyId),
		BankAccountID:    accountId,
	}, nil
}

CryptoInvestmentDBFactory is a Factory located within the infrastructure layer, and it is used to reconstruct the CryptoInvestment Entity. In this example, there is only a method for transforming a DAO to an Entity, but the same Factory can have a method for transforming an Entity into a DAO as well. Since CryptoInvestmentDBFactory uses structures from both the infrastructure (CryptoInvestmentGorm) and the domain (CryptoInvestment), it must reside within the infrastructure layer. This is because we cannot have any dependencies on other layers inside the domain layer.

I always prefer to use UUIDs within the business logic and expose only UUIDs in the API response. However, databases do not typically support really well strings or binaries as primary keys, so the Factory is the appropriate place to handle this conversion.

Conclusion

The Factory pattern is a concept rooted in older design patterns from The Gang of Four. It can be implemented as an Abstract Factory or a Factory Method. We use it in cases when we want to separate the creation logic from other business logic. Additionally, we can utilize it to transform our Entities to DTOs and vice versa.

Useful Resources

comments powered by Disqus