Writing idiomatic Go using
Domain Driven Design

Who am I?

Damiano Petrungaro

Italy

source: www.vidiani.com

Tivoli

Tivoli (RM)

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

  • 88
Loading comments...

More from Damiano Petrungaro