Practical DDD in Golang: Factory
- 5 minutes read - 1018 wordsThe 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.