Writing idiomatic Go using
Domain Driven Design
Who am I?
Damiano Petrungaro
Italy
source: www.vidiani.com
Tivoli
source: www.livitaly.com
source: www.tivolitouring.com
source: www.romaest.org
Tivoli (RM)
source: www.confinelive.it
source: www.greenparkmadama.it
Me everyday:
Once upon a time...
I mean, it's PHP...
BUT
its community has plenty of
Domain Driven Design
advocates
Time passed by...
PHP is OOP
Golang is not OOP
DDD is.... both and none!
DDD does not resonate in Golang
End goal:
Writing idiomatic Go code using DDD patterns
without ending up with an OOP application written in Go
DDD
Two sides for one love
DDD
Strategic Design
Tactical Design
It is the one in which you and the domain experts analyze a domain, define its bounded contexts, and look for the best way to let them communicate.
As you can easily assume, the strategic design is programming language agnostic.
Describes a group of patterns to use to shape as code the invariants and models defined in a domain analysis, often driven by the strategic design.
The end goal of applying those patterns is to model the code in a simple but expressive and safe way.
Strategic Design
Packaging and bounded contexts
Strategic Design
Packaging and bounded contexts
package main
import "github.com/company/email"
func main() {
aEmail, err := email.NewAdminEmail("info@whatever.com")
//...
cEmail, err := email.NewCustomerEmail("name.surname@gmail.com")
//...
}
Grouping by kind
Strategic Design
Packaging via bounded contexts
package main
import (
"github.com/company/service/admin"
"github.com/company/service/customer"
)
func main() {
aEmail, err := admin.NewEmail("info@whatever.com")
//...
cEmail, err := customer.NewEmail("name.surname@gmail.com")
//...
}
Grouping by context
Strategic Design
Packaging and bounded contexts
📂 app
┣ 📦customer
┃ ┗ 📜customer.go
┣ 📦events
┃ ┣ 📜customer_events.go
┃ ┣ 📜product_events.go
┃ ┗ 📜user_events.go
┣ 📦product
┃ ┗ 📜product.go
┗ 📦user
┗ 📜user.go
Strategic Design
Packaging via bounded contexts
📂 app
┣ 📦customer
┃ ┣ 📜 events.go
┃ ┗ 📜customer.go
┣ 📦product
┃ ┣ 📜 events.go
┃ ┗ 📜product.go
┗ 📦user
┣ 📜 events.go
┗ 📜user.go
Strategic Design
Patterns and communication by design
Strategic Design
Strategic patterns and communication by design
📂 app
┣ 📦delivery
┃ ┗ 📜 delivery.go // may import product
┗ 📦 product
┗ 📜 product.go // or may import delivery
Strategic Design
Anti-Corruption Layer
📂 app
┣ 📦delivery
┃ ┗ 📜delivery.go // imports pubsub
┣ 📦product
┃ ┗ 📜product.go // imports pubsub
┗ 📦pubsub
┗ 📜pubsub.go
Strategic Design
Strategic patterns and communication by design
📂 app
┣ 📦customer
┃ ┣ 📜 events.go
┃ ┗ 📜customer.go
┣ 📦product
┃ ┣ 📜 events.go
┃ ┗ 📜product.go
┗ 📦user
┣ 📜 events.go
┗ 📜user.go
Strategic Design
📂 app
┣ 📦customer
┃ ┣ 📦event
┃ ┃ ┗ 📜registered.go
┃ ┃ ┗ 📜activated.go
┃ ┃ ┗ 📜banned.go
┃ ┗ 📜customer.go
┣ 📦product
┃ ┣ 📦event
┃ ┃ ┗ 📜added.go
┃ ┃ ┗ 📜removed.go
┃ ┃ ┗ 📜published.go
┃ ┗ 📜product.go
┗ 📦user
┣ 📦event
┃ ┗ 📜logged.go
┃ ┗ 📜signed.go
┗ 📜user.go
Customer/Supplier
Strategic Design
Strategic patterns and communication by design
THE LEGACY CODE
Strategic Design
Big ball of mud
A little copying is better than a little dependency
Strategic Design
Tactical Design
It's all about code... almost.
Tactical Design
It's all about code... almost.
- Value Type (AKA Value Object )
 - Repository
Tactical Design
It's all about code... almost.
Tactical Design
THE ALWAYS VALID STATE
Tactical Design
// tab/tab.go
package tab
type Tab struct {
Title string
}
// cmd/app/main.go
package main
import "tab"
func main() {
t := Tab{Title: ""}
// ...
}
The always valid state, the unachievable always valid state
Tactical Design
// tab/tab.go
package tab
import (
"errors"
)
type Tab struct {
Title string
}
func New(t string) (*Tab, error) {
switch l := len(t); {
case l < 1:
return nil, errors.New("tab: could not use title less than 1 char")
case l > 50:
return nil, errors.New("tab: could not use title more than 50 char")
default:
return &Tab{Title: t}, nil
}
}
// cmd/app/main.go
package main
import "tab"
func main() {
t, err := Tab.New("a valid title")
if err != nil {
panic(err)
}
t.Title = ""
// ...
}
The always valid state
Tactical Design
// tab/tab.go
package tab
import (
"errors"
)
type Tab struct {
title string
}
func New(t string) (*Tab, error) {
switch l := len(t); {
case l < 1:
return nil, errors.New("tab: could not use title less than 1 char")
case l > 50:
return nil, errors.New("tab: could not use title more than 50 char")
default:
return &Tab{title: t}, nil
}
}
// cmd/app/main.go
package main
import "tab"
func main() {
t, err := tab.New("a valid title")
if err != nil {
panic(err)
}
t2 := &tab.Tab{}
// ...
}
The always valid state
Tactical Design
The always valid state: finding a balance
Tactical Design
Value Type
Tactical Design
Value Type: design
package tab
import (
"errors"
"fmt"
"strings"
)
const (
minTitleLength = 1
maxTitleLength = 50
)
var (
// Errors used when an invalid title is given
ErrInvalidTitle = errors.New("tab: could not use invalid title")
ErrTitleTooShort = fmt.Errorf("%w: min length allowed is %d", ErrInvalidTitle, minTitleLength)
ErrTitleTooLong = fmt.Errorf("%w: max length allowed is %d", ErrInvalidTitle, maxTitleLength)
)
// Title represents a tab title
type Title string
// NewTitle returns a title and an error back
func NewTitle(d string) (Title, error) {
switch l := len(strings.TrimSpace(d)); {
case l < minTitleLength:
return "", ErrTitleTooShort
case l > maxTitleLength:
return "", ErrTitleTooLong
default:
return Title(d), nil
}
}
// String returns a string representation of the title
func (t Title) String() string {
return string(t)
}
// Equals returns true if the titles are equal
func (t Title) Equals(t2 Title) bool {
return t.String() == t2.String()
}
Tactical Design
Value Type: advantages
type addTabReq struct {
Title tab.Title `json:"tab_title"`
}
func (r *addTabReq) UnmarshalJSON(data []byte) error {
type clone addTabReq
var req clone
if err := json.Unmarshal(data, &req); err != nil {
return err
}
var err error
if r.Title, err = tab.NewTitle(req.Title.String()); err != nil {
return err
}
return nil
}
Tactical Design
Value Type: design
package tab
import (
"errors"
"fmt"
"strings"
)
const (
minTitleLength = 1
maxTitleLength = 50
)
var (
// Errors used when an invalid title is given
ErrInvalidTitle = errors.New("tab: could not use invalid title")
ErrTitleTooShort = fmt.Errorf("%w: min length allowed is %d", ErrInvalidTitle, minTitleLength)
ErrTitleTooLong = fmt.Errorf("%w: max length allowed is %d", ErrInvalidTitle, maxTitleLength)
)
// Title represents a tab title
type Title string
// NewTitle returns a title and an error back
func NewTitle(d string) (Title, error) {
switch l := len(strings.TrimSpace(d)); {
case l < minTitleLength:
return "", ErrTitleTooShort
case l > maxTitleLength:
return "", ErrTitleTooLong
default:
return Title(d), nil
}
}
// String returns a string representation of the title
func (t Title) String() string {
return string(t)
}
// Equals returns true if the titles are equal
func (t Title) Equals(t2 Title) bool {
return t.String() == t2.String()
}
Tactical Design
Value Type: where to place it
📂 app
┗ 📦tab
┣ 📜 tab.go
┗ 📜 title.go
Tactical Design
Repository
Tactical Design
Repository: design
package tab
import "errors"
var (
//Errors returned by the repository
ErrRepoNextID = errors.New("tab: could not return next id")
ErrRepoList = errors.New("tab: could not list")
ErrNotFound = errors.New("tab: could not find")
ErrRepoGet = errors.New("tab: could not get")
ErrRepoAdd = errors.New("tab: could not add")
ErrRepoRemove = errors.New("tab: could not remove")
)
type Repo interface {
// NextID returns the next free ID and an error in case of failure
NextID() (ID, error)
// List returns a tab slice and an error in case of failure
List() ([]*Tab, error)
// Find returns a tab or nil if it is not found and an error in case of failure
Find(ID) (*Tab, error)
// Get returns a tab and error in case is not found or failure
Get(ID) (*Tab, error)
// Add persists a tab (already existing or not) and returns an error in case of failure
Add(*Tab) error
// Remove removes a tab and returns and error in case is not found or failure
Remove(ID) error
}
Tactical Design
Repository: design
package tab
// ...
type ReadRepo interface {
// List returns a tab slice and an error in case of failure
List() ([]*Tab, error)
// Find returns a tab or nil if it is not found and an error in case of failure
Find(ID) (*Tab, error)
// Get returns a tab and error in case is not found or failure
Get(ID) (*Tab, error)
}
type WriteRepo interface {
// NextID returns the next free ID and an error in case of failure
NextID() (ID, error)
// Add persists a tab (already existing or not) and returns an error in case of failure
Add(*Tab) error
// Remove removes a tab and returns and error in case is not found or failure
Remove(ID) error
}
Tactical Design
Repository: design
package tab
import "errors"
var (
//Errors returned by the repository
ErrRepoNextID = errors.New("tab: could not return next id")
ErrRepoList = errors.New("tab: could not list")
ErrNotFound = errors.New("tab: could not find")
ErrRepoGet = errors.New("tab: could not get")
ErrRepoAdd = errors.New("tab: could not add")
ErrRepoRemove = errors.New("tab: could not remove")
)
type Repo interface {
// NextID returns the next free ID and an error in case of failure
NextID() (ID, error)
// List returns a tab slice and an error in case of failure
List() ([]*Tab, error)
// Find returns a tab or nil if it is not found and an error in case of failure
Find(ID) (*Tab, error)
// Get returns a tab and error in case is not found or failure
Get(ID) (*Tab, error)
// Add persists a tab (already existing or not) and returns an error in case of failure
Add(*Tab) error
// Remove removes a tab and returns and error in case is not found or failure
Remove(ID) error
}
Tactical Design
Repository: design
func (r *MysqlRepo) Add(t *Tab) error {
// ...
return fmt.Errorf("%w: %s", tab.ErrRepoAdd, "a more detailed reason here")
}
Tactical Design
Repository: design
package tab
import "errors"
var (
//Errors returned by the repository
ErrRepoNextID = errors.New("tab: could not return next id")
ErrRepoList = errors.New("tab: could not list")
ErrNotFound = errors.New("tab: could not find")
ErrRepoGet = errors.New("tab: could not get")
ErrRepoAdd = errors.New("tab: could not add")
ErrRepoRemove = errors.New("tab: could not remove")
)
type Repo interface {
// NextID returns the next free ID and an error in case of failure
NextID() (ID, error)
// List returns a tab slice and an error in case of failure
List() ([]*Tab, error)
// Find returns a tab or nil if it is not found and an error in case of failure
Find(ID) (*Tab, error)
// Get returns a tab and error in case is not found or failure
Get(ID) (*Tab, error)
// Add persists a tab (already existing or not) and returns an error in case of failure
Add(*Tab) error
// Remove removes a tab and returns and error in case is not found or failure
Remove(ID) error
}
Tactical Design
Repository: hint
ts, errr := repo.ListActive()
ts, errr := repo.List(repo.Filter{"active": true})
Don't
Do
Tactical Design
Repository: where to place it
📂 app
┣ 📦internal
┃ ┗ 📦tab
┃ ┗ 📜repo.go // here a MySQL implementation
┗ 📦tab
┗ 📜repo.go // here a the interface and the errors
Tactical Design
Repository: where to place it
📂 app
┣ 📦internal
┃ ┗ 📦tab
┃ ┗ 📜repo.go // here a MySQL implementation
┗ 📦tab
┗ 📜repo.go // here a the interface and the errors
- Entity
 - Aggregate
 - Aggregate Root
 - Domain Service
Tactical Design
More patterns
Conclusion
- DDD helps us structuring and modeling all the time
 - Idiomatic Go code using DDD is achievable
 - Applying DDD enhance the languange mechanisms
twitter: @damiano_dev
email: damianopetrungaro@gmail.com
Writing idiomatic Go using Domain Driven Design
By Damiano Petrungaro
Writing idiomatic Go using Domain Driven Design
- 1,701