Start Using The Specification Pattern In Golang Now!

If you have experience in software development, you’ve likely encountered the need for validation. The task is often straightforward in the beginning, but as the complexity of the rules compound, it can quickly become challenging. Having an effective strategy in place is the best way to avoid validation hell.

This is where the Specification Pattern comes in. It is a design pattern derived from Domain-Driven Design that provides a solution to handling complex validation rules.

In this blog post, we will explore the Specification Pattern, discuss the problem it solves, and provide Go code examples that can be used in your projects today.

Table Of Contents

The Problem

To get started, let’s look at the problem the Specification Pattern solves. The easiest way to explain is through a story which may be relatable to some readers. Although fictional, the story mirrors a common scenario in software development.

We worked for a university which was starting to build a lot of in-house software. Our task was to create an application that filters out ineligible applicants for our courses. Filtering was a manual process, so for an initial release, any reduction in applicants would be considered progress.

With the deadline approaching for course applications, the pressure was on to deliver in a timely manner. We decided to put all of our validation rules into a single function. It was only to check they were over eighteen and lived in an accepted area code, so it wasn’t an issue.

Fast forward three months since the release and additional validation rules were requested. The rules were related to the applicant’s grades. Each grade would be worth a specific number of points and each course had a point requirement to be accepted. Since there were only five courses, it was simple enough to add this logic to our validation function.

At this stage, we didn’t anticipate further validation rules would be required. We were mistaken, as we received yet another request. The number of points varied by subject and some courses did not accept points from certain subjects. This added a little more complexity, but nothing that couldn’t be handled in our validation function.

Some time had passed, and we received a request for extra validation rules. This time, the point requirements for the courses had to be lowered by 20% if the applicant lived in a specific area code. We were also asked to add five new courses. We just about managed to “slot” the logic in the validation function.

I’m hoping by now you’re starting to see a problem develop. To add new validation rules, we have to try and “slot” in the logic. This is because all of the rules are tightly coupled. Eventually, it will become time-consuming to add simple validation rules, with the possibility of testing all edge cases slowly diminishing.

The code cannot scale and an expensive refactor is imminent. Once it became clear the complexity of the validation rules was going to continue to increase, plans for dealing with them should’ve been devised. Let me introduce you to the Specification Pattern.

What Is The Specification Pattern?

The Specification Pattern is a design pattern commonly used in Domain-Driven Design (DDD) to encapsulate validation rules for a domain. In Layman’s terms, a domain in DDD can be considered the subject. For example, students would be the domain in a student management API.

It’s important to note that DDD is a complex topic that cannot be fully explained within the scope of this blog post. The creator of DDD, Eric Evans, wrote a book that spans over 500 pages to explain the concept in detail. In case you’re not up for reading a book, the presentation by the author on YouTube is highly recommended.

A specification is a single validation rule, such as requiring a student to be at least 18 years old. Each specification operates in isolation, making the code readable, extendable, and most importantly, testable.

Let’s take a look at how the code could be structured. Each specification will be a struct which implements a common interface. This interface will consist of a single method which takes an applicant as input and returns a boolean value to determine if the specification passes. The method is commonly called IsSatisfiedBy.

In Go, we achieve polymorphism using interfaces. This allows us to create an array of specifications, loop through them, and determine if all specs have passed. We can even create a facade struct that handles this logic.

Hopefully, an example will clear up any confusion.

Example Implementation

Our local football team has been accepting online applications for new players. Unfortunately, not all applicants meet the requirements. To be eligible for the team, the applicant must meet the following criteria:

  • Be at least 18 years old.
  • Be male.
  • Have no health conditions.
  • Live in London.

We need to write a Go application which can filter out all of the invalid applicants. While it is possible to accomplish this task without using the Specification Pattern, this should provide a good example of how powerful it can be.

Create a new Go module.

mkdir specification-pattern-example && cd $_ && go mod init
Bash

Add the dataset.

applicants.json

[
  {
    "name": "James Smith",
    "age": 17,
    "gender": "m",
    "health_conditions": [],
    "location": "London"
  },
  {
    "name": "Daniella Williams",
    "age": 18,
    "gender": "f",
    "health_conditions": [],
    "location": "London"
  },
  {
    "name": "Joshua Brown",
    "age": 21,
    "gender": "m",
    "health_conditions": [
      "Heart"
    ],
    "location": "London"
  },
  {
    "name": "Matthew Davis",
    "age": 25,
    "gender": "m",
    "health_conditions": [],
    "location": "Manchester"
  },
  {
    "name": "Joseph Jones",
    "age": 19,
    "gender": "m",
    "health_conditions": [],
    "location": "London"
  },
  {
    "name": "Thomas Miller",
    "age": 20,
    "gender": "m",
    "health_conditions": [],
    "location": "London"
  },
  {
    "name": "Michael Johnson",
    "age": 21,
    "gender": "m",
    "health_conditions": [],
    "location": "London"
  },
  {
    "name": "William Wilson",
    "age": 21,
    "gender": "m",
    "health_conditions": [],
    "location": "London"
  },
  {
    "name": "Alexander Moore",
    "age": 19,
    "gender": "m",
    "health_conditions": [],
    "location": "London"
  },
  {
    "name": "Christopher Rodriguez",
    "age": 28,
    "gender": "m",
    "health_conditions": [],
    "location": "London"
  }
]
JSON

Create a model package and Applicant struct. 

model/model.go

package model

type Applicant struct {
	Name             string   `json:"name"`
	Age              int      `json:"age"`
	Gender           string   `json:"gender"`
	HealthConditions []string `json:"health_conditions"`
	Location         string   `json:"location"`
}
Go

Let’s make sure everything is working. Create a main package which reads the JSON file, unmarshals it into a slice of Applicant and prints each of their names. 

main.go

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"

	"github.com/thegodev/specification-pattern-example/model"
)

func main() {
	f, err := os.ReadFile("./applicants.json")
	if err != nil {
		log.Fatal("Unable to read file")
	}

	var applicants []model.Applicant
	if err := json.Unmarshal(f, &applicants); err != nil {
		log.Fatal("Unable to marshal into struct")
	}

	for _, applicant := range applicants {
		fmt.Println(applicant.Name)
	}
}
Go

Run the application.

go run main.go
Bash

Ensure the name of every applicant is printed.

James Smith
Daniella Williams
Joshua Brown
Matthew Davis
Joseph Jones
Thomas Miller
Michael Johnson
William Wilson
Alexander Moore
Christopher Rodriguez
Bash

Create a config package and struct to hold the boundaries of our validation rules. 

config/config.go

package config

type Applicant struct {
	MinAge         int
	ValidGenders   []string
	ValidLocations []string
}

func NewApplicant() *Applicant {
	return &Applicant{
		MinAge:         18,
		ValidGenders:   []string{"m"},
		ValidLocations: []string{"London"},
	}
}
Go

It’s time to create the specification package.

Firstly, we’re going to create an interface for the specifications to implement. Let’s call it Applicant. It contains a single method IsSatisfiedBy which takes an applicant struct and returns a boolean.

Next, we’ll create our facade struct which will also implement the Applicant interface. It will contain a field called specs which is a slice of the Applicant interface. The IsSaisfiedBy method will loop through the specs and return false if any of the specs fail. This is an AND specification, so we’ll call it AndApplicant.

specification/specification.go

package specification

import "github.com/thegodev/specification-pattern-example/model"

type Applicant interface {
	IsSatisfiedBy(a model.Applicant) bool
}

type AndApplicant struct {
	specs []Applicant
}

func (a AndApplicant) IsSatisfiedBy(app model.Applicant) bool {
	for _, spec := range a.specs {
		if !spec.IsSatisfiedBy(app) {
			return false
		}
	}
	return true
}

func NewAndApplicant(specs ...Applicant) AndApplicant {
	return AndApplicant{
		specs: specs,
	}
}
Go

Now we can write each of the specifications. Let’s begin with Age to check the applicant is over 18 years old.

specification/age.go

package specification

import "github.com/thegodev/specification-pattern-example/model"

type Age struct {
	minAge int
}

func NewAge(minAge int) Age {
	return Age{
		minAge: minAge,
	}
}

func (s Age) IsSatisfiedBy(a model.Applicant) bool {
	return a.Age >= s.minAge
}
Go

Next is the Gender specification to check the applicant is male.

specification/gender.go

package specification

import (
	"slices"

	"github.com/thegodev/specification-pattern-example/model"
)

type Gender struct {
	validGenders []string
}

func NewGender(validGenders []string) Gender {
	return Gender{
		validGenders: validGenders,
	}
}

func (g Gender) IsSatisfiedBy(a model.Applicant) bool {
	return slices.Contains(g.validGenders, a.Gender)
}
Go

Now create the Location specification to check the applicant lives in London. 

specification/location.go

package specification

import (
	"slices"

	"github.com/thegodev/specification-pattern-example/model"
)

type Location struct {
	validLocations []string
}

func NewLocation(validLocations []string) Location {
	return Location{
		validLocations: validLocations,
	}
}

func (l Location) IsSatisfiedBy(a model.Applicant) bool {
	return slices.Contains(l.validLocations, a.Location)
}
Go

Finally, write the HealthConditions specification to check the applicant has no health conditions. 

specification/health_conditions.go

package specification

import "github.com/thegodev/specification-pattern-example/model"

type HealthConditions struct{}

func (h HealthConditions) IsSatisfiedBy(a model.Applicant) bool {
	return len(a.HealthConditions) == 0
}
Go

With the specifications in place, we can now filter the applicants in the main package.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"

	"github.com/thegodev/specification-pattern-example/config"
	"github.com/thegodev/specification-pattern-example/model"
	"github.com/thegodev/specification-pattern-example/specification"
)

func filter(as []model.Applicant, spec specification.Applicant) []model.Applicant {
	result := make([]model.Applicant, 0, len(as))
	for _, a := range as {
		if spec.IsSatisfiedBy(a) {
			result = append(result, a)
		}
	}
	return result
}

func main() {
	f, err := os.ReadFile("./applicants.json")
	if err != nil {
		log.Fatal("unable to read file")
	}

	var applicants []model.Applicant
	if err := json.Unmarshal(f, &applicants); err != nil {
		log.Fatal("unable to marshal into struct")
	}

	cfg := config.NewApplicant()

	spec := specification.NewAndApplicant(
		specification.NewAge(cfg.MinAge),
		specification.NewGender(cfg.ValidGenders),
		specification.NewLocation(cfg.ValidLocations),
		specification.HealthConditions{},
	)

	filtered := filter(applicants, spec)

	for _, applicant := range filtered {
		fmt.Println(applicant.Name)
	}
}
Go

Run the application.

go run main.go
Bash

Check only the valid applicants remain.

Joseph Jones
Thomas Miller
Michael Johnson
William Wilson
Alexander Moore
Christopher Rodriguez
Bash

Congratulations! You have successfully implemented the Specification Pattern to filter out invalid applicants for a football team. Although this was a simple example, hopefully it demonstrated the benefits of the pattern.

You could make several adjustments to the example above to suit your needs. A few examples include:

  • Changing the specification from AND to OR.
  • Altering the IsSatisfiedBy method to return an error instead of a boolean. This would help you identify which specification failed.
  • Make the IsSatisfiedBy method variadic so that you can pass in one or more applicants at a time.

The source code is available on our GitHub.

Conclusion

In conclusion, the Specification Pattern should be considered an effective solution for handling complex validation rules. The pattern keeps the rules decoupled, which makes it easy to extend and test. This is a pattern you want to be aware of when designing a new system.

It’s worth noting that the Specification Pattern isn’t always the best approach. Depending on the scale of your application, using the pattern could be unnecessary. If your validation rules are unlikely to become bountiful or complex, it may be more appropriate to handle them with if-else statements or functions.

If you have any questions or comments, please leave them below. Also, don’t forget to subscribe to our newsletter to receive more informative posts like this directly to your inbox.

If you found this article helpful, please share it with your friends and colleagues.

Thank you for reading.

2 responses

  1. Cristian Nicola avatar
    Cristian Nicola

    should the Or be called And?

    or returns true if any is true. or return false of all are false.

    and returns true if all are true. and returns false if any is false…

    1. Yes, you are right, thank you for pointing that out. I have fixed the post and the code.

Leave a Reply

Your email address will not be published. Required fields are marked *