π spec2go
A Go implementation of the Specification Pattern for composable, reusable business rules.
Stop scattering validation logic across your codebase. spec2go lets you define small, testable business rules as specifications and combine them into policies β making your domain logic explicit, reusable, and easy to reason about.

π Table of Contents
π Overview
spec2go provides a clean, type-safe way to define and evaluate business rules using Go generics. Instead of scattering validation logic throughout your codebase, you define atomic specifications that can be composed into policies.
// Define failure reasons as typed constants
type Reason string
const (
Underage Reason = "UNDERAGE"
EmailNotVerified Reason = "EMAIL_NOT_VERIFIED"
)
// Define specifications
isAdult := spec.New("IsAdult", func(u User) bool { return u.Age >= 18 }, Underage)
hasVerifiedEmail := spec.New("HasVerifiedEmail", func(u User) bool { return u.EmailVerified }, EmailNotVerified)
// Build a policy
registrationPolicy := spec.NewPolicy[User, Reason]().
With(isAdult).
With(hasVerifiedEmail)
// Evaluate
result := registrationPolicy.EvaluateFailFast(user)
if result.AllPassed() {
// proceed
} else {
// handle result.FailureReasons()
}
β¨ Features
- π§© Composable β Build complex rules from simple, reusable specifications
- π Type-safe β Failure reasons use Go generics (
R comparable), not bare strings
- β‘ Two evaluation modes β
EvaluateFailFast (stops on first failure) or EvaluateAll (collects all failures)
- π Logical operators β
AllOf, AnyOf, AnyOfAll, Not for combining specifications
- πΉ Idiomatic Go β Generics, interfaces, package-level functions,
fmt.Stringer
- π« Zero dependencies β Pure Go standard library only
π¦ Installation
go get github.com/caik/spec2go
import "github.com/caik/spec2go/pkg/spec"
π Core Concepts
π Specification
A single, atomic condition that evaluates a context and returns pass/fail with a reason.
Simple spec via New:
minimumAge := spec.New("MinimumAge",
func(a LoanApplication) bool { return a.ApplicantAge >= 18 },
ReasonApplicantTooYoung,
)
Custom struct spec (for complex logic or multiple failure reasons):
type DocumentCheck struct {
spec.NamedSpec[Claim, ClaimReason]
}
func (s DocumentCheck) Evaluate(ctx Claim) spec.SpecificationResult[ClaimReason] {
var missing []ClaimReason
if !ctx.HasIDDocument {
missing = append(missing, MissingID)
}
if !ctx.HasProofOfLoss {
missing = append(missing, MissingProofOfLoss)
}
if len(missing) == 0 {
return spec.Pass[ClaimReason](s.Name())
}
return spec.Fail(s.Name(), missing...)
}
π Policy
An ordered collection of specifications evaluated as a unit:
loanPolicy := spec.NewPolicy[LoanApplication, Reason]().
With(minimumAge).
With(maximumAge).
With(creditCheck)
// Fail-fast: stop at first failure (best for performance)
result := loanPolicy.EvaluateFailFast(application)
// Evaluate all: collect every failure (best for showing all errors to the user)
result := loanPolicy.EvaluateAll(application)
π Composites
Combine specifications with logical operators:
// AND β all must pass (always evaluates all)
fullyVerified := spec.AllOf("FullyVerified", emailVerified, phoneVerified)
// OR β at least one must pass (short-circuits on first pass)
hasPayment := spec.AnyOf("HasPayment", hasCreditCard, hasBankAccount)
// OR β at least one must pass (evaluates all, no short-circuit)
hasPayment := spec.AnyOfAll("HasPayment", hasCreditCard, hasBankAccount)
// NOT β inverts the result
notBlocked := spec.Not("NotBlocked", ReasonBlocked, isBlockedCountry)
Composites can be nested arbitrarily and produce human-readable expressions:
fmt.Println(loanPolicy.String())
// (MinimumAge AND (GoodCreditScore OR (SufficientIncome AND IsEmployed)))
π‘ Failure Reasons
Since Go has no enums, define failure reasons as typed constants β any comparable type works:
// Typed string constants (recommended β prints meaningfully without extra code)
type LoanReason string
const (
AgeTooYoung LoanReason = "AGE_TOO_YOUNG"
InsufficientIncome LoanReason = "INSUFFICIENT_INCOME"
PoorCreditScore LoanReason = "POOR_CREDIT_SCORE"
)
// Typed int with iota (more memory-efficient for large sets)
type LoanReason int
const (
AgeTooYoung LoanReason = iota
InsufficientIncome
PoorCreditScore
)
π― Examples
The examples/ directory contains complete working examples:
# Loan eligibility β basic specs + AnyOf/AllOf nesting
go run ./examples/loan/
# E-commerce order validation β custom struct spec + Not
go run ./examples/ecommerce/
# Feature access control β multiple policies + dynamic spec factories
go run ./examples/accesscontrol/
π οΈ Building
# Build all packages
go build ./...
# Run all tests
go test ./...
# Run tests with race detector and coverage
go test ./... -race -coverprofile=coverage.out
# View coverage report
go tool cover -html=coverage.out
π€ Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
βοΈ License

Released 2026 by Carlos Henrique Severino