Skip to main content
  1. Articles/

Practical DDD in Golang: Specification

·1596 words·8 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 9: This Article

There are not many code structures that bring me joy whenever I need to write them. The first time I implemented such code was with a lightweight ORM in Go, back when we didn’t have one. However, I used ORM for many years, and at some point, when you rely on ORM, using QueryBuilder becomes inevitable. Here, you may notice terms like “predicates”, and that’s where we can find the Specification pattern. It’s hard to find any pattern we use as Specification, yet we do not hear its name. I think the only thing harder is to write an application without using this pattern. The Specification has many applications. We can use it for querying, creation, or validation. We may provide a unique code that can do all this work or provide different implementations for different use cases.

For Validation
#

The first use case for the Specification pattern is validation. Typically, we validate data in forms, but this is at the presentation level. Sometimes, we perform validation during creation, such as for Value Objects. In the context of the domain layer, we can use Specifications to validate the states of Entities and filter them from a collection. So, validation at the domain layer has a broader meaning than for user inputs.

Base Product Specification

type Product struct {
	ID            uuid.UUID
	Material      MaterialType
	IsDeliverable bool
	Quantity      int
}

type ProductSpecification interface {
	IsValid(product Product) bool
}

A simple Product Specification

type HasAtLeast struct {
	pieces int
}

func NewHasAtLeast(pieces int) ProductSpecification {
	return HasAtLeast{
		pieces: pieces,
	}
}

func (h HasAtLeast) IsValid(product Product) bool {
	return product.Quantity >= h.pieces
}

In the example above, there is an interface called ProductSpecification. It defines only one method, IsValid, which expects instances of Product and returns a boolean value as a result if the Product passes validation rules. A simple implementation of this interface is HasAtLeast, which verifies the minimum quantity of the Product.

Function as a Product Specification

type FunctionSpecification func(product Product) bool

func (fs FunctionSpecification) IsValid(product Product) bool {
	return fs(product)
}

func IsPlastic(product Product) bool {
	return product.Material == Plastic
}

func IsDeliverable(product Product) bool {
	return product.IsDeliverable
}

More interesting validators are two functions, IsPlastic and IsDeliverable. We can wrap those functions with a specific type, FunctionSpecification. This type embeds a function with the same signature as the two mentioned. Besides that, it provides methods that respect the ProductSpecification interface. This example is a nice feature of Go, where we can define a function as a type and attach a method to it so that it can implicitly implement some interface. In this case, it exposes the method IsValid, which executes the embedded function.

Combine Product Specification

type AndSpecification struct {
	specifications []ProductSpecification
}

func NewAndSpecification(specifications ...ProductSpecification) ProductSpecification {
	return AndSpecification{
		specifications: specifications,
	}
}

func (s AndSpecification) IsValid(product Product) bool {
	for _, specification := range s.specifications {
		if !specification.IsValid(product) {
			return false
		}
	}
	return true
}

type OrSpecification struct {
	specifications []ProductSpecification
}

func NewOrSpecification(specifications ...ProductSpecification) ProductSpecification {
	return OrSpecification{
		specifications: specifications,
	}
}

func (s OrSpecification) IsValid(product Product) bool {
	for _, specification := range s.specifications {
		if specification.IsValid(product) {
			return true
		}
	}
	return false
}

type NotSpecification struct {
	specification ProductSpecification
}

func NewNotSpecification(specification ProductSpecification) ProductSpecification {
	return NotSpecification{
		specification: specification,
	}
}

func (s NotSpecification) IsValid(product Product) bool {
	return !s.specification.IsValid(product)
}

In addition, there is also one unique Specification, AndSpecification. Such a struct helps us use an object that implements the ProductSpecification interface but groups validation from all Specifications included.

In the code snippet above, we may find two additional Specifications. One is OrSpecification, and it, like AndSpecification, executes all Specifications which it holds. Just, in this case, it uses the “or” algorithm instead of “and”. The last one is NotSpecification, which negates the result of the embedded Specification. NotSpecification can also be a functional Specification, but I did not want to complicate it too much.

Test all together

func main() {
	spec := NewAndSpecification(
		NewHasAtLeast(10),
		FunctionSpecification(IsPlastic),
		FunctionSpecification(IsDeliverable),
	)

	fmt.Println(spec.IsValid(Product{}))
	// output: false

	fmt.Println(spec.IsValid(Product{
		Material:      Plastic,
		IsDeliverable: true,
		Quantity:      50,
	}))
	// output: true
}

For Querying
#

I have already mentioned in this article the application of the Specification pattern as part of ORM. In many cases, you will not need to implement Specifications for this use case, at least if you use any ORM. Excellent implementations of Specification, in the form of predicates, I found in the Ent library from Facebook. From that moment, I did not have a use case to write Specifications for querying. Still, when you find out that your query for Repository on the domain level can be too complex, you need more possibilities to filter desired Entities. Implementation can look like the example below.

One big example for Querying

type Product struct {
	ID            uuid.UUID
	Material      MaterialType
	IsDeliverable bool
	Quantity      int
}

type ProductSpecification interface {
	Query() string
	Value() []interface{}
}

type AndSpecification struct {
	specifications []ProductSpecification
}

func NewAndSpecification(specifications ...ProductSpecification) ProductSpecification {
	return AndSpecification{
		specifications: specifications,
	}
}

func (s AndSpecification) Query() string {
	var queries []string
	for _, specification := range s.specifications {
		queries = append(queries, specification.Query())
	}

	query := strings.Join(queries, " AND ")

	return fmt.Sprintf("(%s)", query)
}

func (s AndSpecification) Value() []interface{} {
	var values []interface{}
	for _, specification := range s.specifications {
		values = append(values, specification.Value()...)
	}
	return values
}

type OrSpecification struct {
	specifications []ProductSpecification
}

func NewOrSpecification(specifications ...ProductSpecification) ProductSpecification {
	return OrSpecification{
		specifications: specifications,
	}
}

func (s OrSpecification) Query() string {
	var queries []string
	for _, specification := range s.specifications {
		queries = append(queries, specification.Query())
	}

	query := strings.Join(queries, " OR ")

	return fmt.Sprintf("(%s)", query)
}

func (s OrSpecification) Value() []interface{} {
	var values []interface{}
	for _, specification := range s.specifications {
		values = append(values, specification.Value()...)
	}
	return values
}

type HasAtLeast struct {
	pieces int
}

func NewHasAtLeast(pieces int) ProductSpecification {
	return HasAtLeast{
		pieces: pieces,
	}
}

func (h HasAtLeast) Query() string {
	return "quantity >= ?"
}

func (h HasAtLeast) Value() []interface{} {
	return []interface{}{h.pieces}
}

func IsPlastic() string {
	return "material = 'plastic'"
}

func IsDeliverable() string {
	return "deliverable = 1"
}

type FunctionSpecification func() string

func (fs FunctionSpecification) Query() string {
	return fs()
}

func (fs FunctionSpecification) Value() []interface{} {
	return nil
}

func main() {

	spec := NewOrSpecification(
		NewAndSpecification(
			NewHasAtLeast(10),
			FunctionSpecification(IsPlastic),
			FunctionSpecification(IsDeliverable),
		),
		NewAndSpecification(
			NewHasAtLeast(100),
			FunctionSpecification(IsPlastic),
		),
	)

	fmt.Println(spec.Query())
	// output: ((quantity >= ? AND material = 'plastic' AND deliverable = 1) OR (quantity >= ? AND material = 'plastic'))

	fmt.Println(spec.Value())
	// output: [10 100]
}

In the new implementation, the ProductSpecification interface provides two methods, Query and Values. We use them to get a query string for a particular Specification and the possible values it holds. Once again, we can see additional Specifications, AndSpecification and OrSpecification. In this case, they join all underlying queries, depending on the operator they present, and merge all values. It is questionable to have such Specifications on the domain layer. As you may see from the output, Specifications provide SQL-like syntax, which delves too much into technical details. In this case, the solution would probably be to define interfaces for different Specifications on the domain layer and have actual implementations on the infrastructure layer. Or to restructure the code so that Specifications hold information about field name, operation, and value. Then, have some mapper on the infrastructure layer that can map such Specifications to an SQL query.

For Creation
#

One simple use case for Specifications is to create a complex object that can vary a lot. In such cases, we can combine it with the Factory pattern or use it inside a Domain Service.

One big example for Creation

type Product struct {
	ID            uuid.UUID
	Material      MaterialType
	IsDeliverable bool
	Quantity      int
}

type ProductSpecification interface {
	Create(product Product) Product
}

type AndSpecification struct {
	specifications []ProductSpecification
}

func NewAndSpecification(specifications ...ProductSpecification) ProductSpecification {
	return AndSpecification{
		specifications: specifications,
	}
}

func (s AndSpecification) Create(product Product) Product {
	for _, specification := range s.specifications {
		product = specification.Create(product)
	}
	return product
}

type HasAtLeast struct {
	pieces int
}

func NewHasAtLeast(pieces int) ProductSpecification {
	return HasAtLeast{
		pieces: pieces,
	}
}

func (h HasAtLeast) Create(product Product) Product {
	product.Quantity = h.pieces
	return product
}

func IsPlastic(product Product) Product {
	product.Material = Plastic
	return product
}

func IsDeliverable(product Product) Product {
	product.IsDeliverable = true
	return product
}

type FunctionSpecification func(product Product) Product

func (fs FunctionSpecification) Create(product Product) Product {
	return fs(product)
}

func main() {
	spec := NewAndSpecification(
		NewHasAtLeast(10),
		FunctionSpecification(IsPlastic),
		FunctionSpecification(IsDeliverable),
	)

	fmt.Printf("%+v", spec.Create(Product{
		ID: uuid.New(),
	}))
	// output: {ID:86c5db29-8e04-4caf-82e4-91d6906cff12 Material:plastic IsDeliverable:true Quantity:10}
}

In the example above, we can find a third implementation of Specification. In this scenario, ProductSpecification supports one method, Create, which expects a Product, adapts it, and returns it back. Once again, there is AndSpecification to apply changes defined by multiple Specifications, but there is no OrSpecification. I could not find an actual use case for the OR algorithm during the creation of an object. Even if it is not present, we can introduce NotSpecification, which could work with specific data types like booleans. Still, in this small example, I could not find a good fit for it.

Conclusion
#

Specification is a pattern that we use everywhere, in many different cases. Today, it isn’t easy to provide validation on the domain layer without the usage of Specifications. Specifications can also be used in querying objects from the underlying storage, and today, they are part of ORM. The third usage is for creating complex instances, where we can combine it with the Factory pattern.

Useful Resources
#

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

Related

Practical DDD in Golang: Repository

·2108 words·10 mins· loading · loading
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.

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