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
Two sides for one love
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.
Packaging and bounded contexts
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
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
Packaging and bounded contexts
📂 app
┣ 📦customer
┃ ┗ 📜customer.go
┣ 📦events
┃ ┣ 📜customer_events.go
┃ ┣ 📜product_events.go
┃ ┗ 📜user_events.go
┣ 📦product
┃ ┗ 📜product.go
┗ 📦user
┗ 📜user.go
Packaging via bounded contexts
📂 app
┣ 📦customer
┃ ┣ 📜 events.go
┃ ┗ 📜customer.go
┣ 📦product
┃ ┣ 📜 events.go
┃ ┗ 📜product.go
┗ 📦user
┣ 📜 events.go
┗ 📜user.go
Patterns and communication by design
Strategic patterns and communication by design
📂 app
┣ 📦delivery
┃ ┗ 📜 delivery.go // may import product
┗ 📦 product
┗ 📜 product.go // or may import delivery
Anti-Corruption Layer
📂 app
┣ 📦delivery
┃ ┗ 📜delivery.go // imports pubsub
┣ 📦product
┃ ┗ 📜product.go // imports pubsub
┗ 📦pubsub
┗ 📜pubsub.go
Strategic patterns and communication by design
📂 app
┣ 📦customer
┃ ┣ 📜 events.go
┃ ┗ 📜customer.go
┣ 📦product
┃ ┣ 📜 events.go
┃ ┗ 📜product.go
┗ 📦user
┣ 📜 events.go
┗ 📜user.go
📂 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 patterns and communication by design
Big ball of mud
A little copying is better than a little dependency
It's all about code... almost.
It's all about code... almost.
It's all about code... almost.
// 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
// 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
// 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
The always valid state: finding a balance
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()
}
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
}
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()
}
Value Type: where to place it
📂 app
┗ 📦tab
┣ 📜 tab.go
┗ 📜 title.go
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
}
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
}
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
}
Repository: design
func (r *MysqlRepo) Add(t *Tab) error {
// ...
return fmt.Errorf("%w: %s", tab.ErrRepoAdd, "a more detailed reason here")
}
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
}
Repository: hint
ts, errr := repo.ListActive()
ts, errr := repo.List(repo.Filter{"active": true})
Don't
Do
Repository: where to place it
📂 app
┣ 📦internal
┃ ┗ 📦tab
┃ ┗ 📜repo.go // here a MySQL implementation
┗ 📦tab
┗ 📜repo.go // here a the interface and the errors
Repository: where to place it
📂 app
┣ 📦internal
┃ ┗ 📦tab
┃ ┗ 📜repo.go // here a MySQL implementation
┗ 📦tab
┗ 📜repo.go // here a the interface and the errors
More patterns
twitter: @damiano_dev
email: damianopetrungaro@gmail.com