SlideShare uma empresa Scribd logo
1 de 169
Baixar para ler offline
roblaszczak
TACTICAL DDD
PATTERNS IN GOLANG
roblaszczak
Hello!
roblaszczak
Hello!
● Co-founder @Tree Dots Labs
roblaszczak
Hello!
● Co-founder @Tree Dots Labs
● Golang, Python, PHP
roblaszczak
TACTICAL DDD
PATTERNS IN GOLANG
roblaszczak
6
+ DDD = ?
roblaszczak
7
roblaszczak
8
roblaszczak
9
roblaszczak
10
Dlaczego to nie działa tak jak
ustaliliśmy?
roblaszczak
11
Myśleliśmy że wiecie że to tak
będzie działać
roblaszczak
12
Czy ktoś w ogóle wie jak to
działa?
roblaszczak
13
Poczekaj, sprawdzę w tym
handlerze na 1k linii i w
triggerach DB
roblaszczak
Nie do końca to mieliśmy na
myśli mówiąc “klient kupił
produkt”...
14
roblaszczak
15
roblaszczak
16
roblaszczak
17
roblaszczak
18
+ DDD =
roblaszczak
19
DOKŁADNE
ODZWIERCIEDLENIE
LOGIKI BIZNESOWEJ
roblaszczak
20
KOD KTÓRY MOŻNA
POKAZAĆ EKSPERTOWI
DOMENOWEMU
roblaszczak
21
BEZPIECZNE
WPROWADZANIE
ZNACZĄCYCH ZMIAN W
LOGICE BIZNESOWEJ
roblaszczak
22
LEGENDARNY
SAMODOKUMENTUJĄCY
SIĘ KOD
roblaszczak
23
UPORZĄDKOWANIE
NAZEWNICTWA
roblaszczak
24
WERYFIKACJA LOGIKI
DOMENOWEJ PRZED
ROZPOCZĘCIEM
IMPLEMENTACJI
roblaszczak
25
LITE DDD =
roblaszczak
26
EVENT STORMING
roblaszczak
27
HOT SPOTS
roblaszczak
28
ENTITY
roblaszczak
29
Nurse administer standard flu
vaccine dose to adult patient.
roblaszczak
30
patient := patient.NewPatient()
patient.SetShotType(vaccine.Flu)
patient.SetDose(10)
patient.SetNurse(nurse)
roblaszczak
31
patient := patient.NewPatient()
patient.SetShotType(vaccine.Flu)
patient.SetDose(10)
patient.SetNurse(nurse)
roblaszczak
32
patient.GiveFluShot()
roblaszczak
33
patient.GiveFluShot()
roblaszczak
34
vaccine :=
vaccine.StandardAdultFluDose()
nurse.AdministerFluVaccine(
patient,
vaccine,
)
roblaszczak
35
Nurse administer standard flu
vaccine dose to adult patient.
roblaszczak
36
vaccine :=
vaccine.StandardAdultFluDose()
nurse.AdministerFluVaccine(
patient,
vaccine,
)
roblaszczak
37
roblaszczak
38
roblaszczak
39
package backlog
type ItemID struct{ id.ID }
type Item struct {
id ItemID
name string
// ...
}
roblaszczak
40
package backlog
type ItemID struct{ id.ID }
type Item struct {
id ItemID
name string
// ...
}
roblaszczak
41
type ItemID struct{ id.ID }
roblaszczak
42
package id
type ID struct {
u ulid.ULID
valid bool
}
func New() ID {
return ID{ulid.MustNew(ulid.Timestamp(time.Now()), rand.Reader), true}
}
func FromString(s string) (ID, error) {}
func (i ID) String() string {}
func (i ID) Bytes() []byte {}
func (i ID) Equals(toCompare ID) bool {}
func (i ID) Empty() bool {}
roblaszczak
43
type ItemID id.ID
roblaszczak
44
type ItemID id.ID
roblaszczak
45
type ItemID struct{ id.ID }
roblaszczak
46
func NewItem(id ItemID, name string) (*Item, error) {
if id.Empty() {
return nil, ErrItemEmptyID
}
i := &Item{id: id}
if err := i.ChangeName(name); err != nil {
return nil, err
}
return i, nil
}
roblaszczak
47
func (i *Item) ChangeName(name string) error
{
if name == "" {
return ErrItemEmptyName
}
i.name = name
return nil
}
roblaszczak
48
var (
ErrItemEmptyName =
domain.NewIllegalStateError("name cannot be empty")
)
roblaszczak
49
func (i *Item) changeName(name string)
error {
if name == "" {
return ErrItemEmptyName
}
i.name = name
return nil
}
roblaszczak
50
func NewItem(id ItemID, name string) (*Item, error) {
if id.Empty() {
return nil, ErrItemEmptyID
}
i := &Item{id: id}
if err := i.ChangeName(name); err != nil {
return nil, err
}
return i, nil
}
roblaszczak
51
{
"valid_item",
args{backlog.ItemID{id.New()}, "foo"},
nil,
}, {
"missing_id",
args{backlog.ItemID{}, "foo"},
backlog.ErrItemEmptyID,
}, {
"missing_name",
args{backlog.ItemID{id.New()}, ""},
backlog.ErrItemEmptyName,
roblaszczak
52
{
"valid_item",
args{backlog.ItemID{id.New()}, "foo"},
nil,
}, {
"missing_id",
args{backlog.ItemID{}, "foo"},
backlog.ErrItemEmptyID,
}, {
"missing_name",
args{backlog.ItemID{id.New()}, ""},
backlog.ErrItemEmptyName,
roblaszczak
53
func TestNewItem(t *testing.T) {
type args struct {
id backlog.ItemID
name string
}
tests := []struct {
name string
args args
wantErr error
}{
roblaszczak
54
t.Run(tt.name, func(t *testing.T) {
_, err := backlog.NewItem(
tt.args.id, tt.args.name
)
assert.Equal(t, tt.wantErr, err)
})
roblaszczak
55
Lifetip: CTRL + Shift + T
roblaszczak
56
package backlog_test
roblaszczak
57
tests := []struct {
name string
args args
wantErr error
}{
roblaszczak
58
func TestItem_ChangeName(t *testing.T) {
roblaszczak
59
roblaszczak
60
{
name: "scheduled_for_release",
item: createTestScheduledForReleaseItem(t),
wantErr: nil,
},
roblaszczak
61
roblaszczak
62
{
name: "scheduled_for_release",
item: createTestScheduledForReleaseItem(t),
wantErr: nil,
},
roblaszczak
63
{
name: "not_scheduled_for_release",
item: createTestItem(t),
wantErr: backlog.ErrMustBeScheduledToCommit,
},
roblaszczak
64
{
name: "already_commited_to_sprint",
item: createTestCommitedToSprintItem(t),
wantErr: nil,
},
roblaszczak
65
func createTestItem(t *testing.T) *backlog.Item {
i, err := backlog.NewItem(
backlog.ItemID{id.New()},
"Foo",
)
require.NoError(t, err)
return i
}
roblaszczak
66
func createTestItem(t *testing.T) *backlog.Item {
i, err := backlog.NewItem(
backlog.ItemID{id.New()},
"Foo",
)
require.NoError(t, err)
return i
}
roblaszczak
67
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
roblaszczak
68
func createTestScheduledForReleaseItem(t *testing.T)
*backlog.Item {
i := createTestItem(t)
err := i.ScheduleRelease(time.Now())
require.NoError(t, err)
return i
}
roblaszczak
69
func createTestCommitedToSprintItem(t *testing.T)
*backlog.Item {
i := createTestScheduledForReleaseItem(t)
s := createTestSprint(t)
err := i.CommitToSprint(s)
require.NoError(t, err)
return i
}
roblaszczak
70
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := createTestSprint(t)
initialSprintID := tt.i.CommitedSprintID()
initialStatus := tt.i.Status()
err := tt.i.CommitToSprint(s)
assert.Equal(t, tt.wantErr, err)
// ...
})
}
roblaszczak
71
if tt.wantErr == nil {
assert.Equal(t, s.ID(), tt.i.CommitedSprintID())
assert.NotEqual(t, s.ID(), initialSprintID)
assert.Equal(
t, tt.i.Status(),
backlog.ItemStatusCommited
)
} else {
assert.Equal(t,initialSprintID,tt.i.CommitedSprintID())
assert.Equal(t, tt.item.Status(), initialStatus)
}
roblaszczak
72
if tt.wantErr == nil {
assert.Equal(t, s.ID(), tt.i.CommitedSprintID())
assert.NotEqual(t, s.ID(), initialSprintID)
assert.Equal(
t, tt.i.Status(),
backlog.ItemStatusCommited
)
} else {
assert.Equal(t,initialSprintID,tt.i.CommitedSprintID())
assert.Equal(t, tt.item.Status(), initialStatus)
}
roblaszczak
73
if tt.wantErr == nil {
assert.Equal(t, s.ID(), tt.i.CommitedSprintID())
assert.NotEqual(t, s.ID(), initialSprintID)
assert.Equal(
t, tt.i.Status(),
backlog.ItemStatusCommited
)
} else {
assert.Equal(t,initialSprintID,tt.i.CommitedSprintID())
assert.Equal(t, tt.item.Status(), initialStatus)
}
roblaszczak
74
if tt.wantErr == nil {
assert.Equal(t, s.ID(), tt.i.CommitedSprintID())
assert.NotEqual(t, s.ID(), initialSprintID)
assert.Equal(
t, tt.i.Status(),
backlog.ItemStatusCommited
)
} else {
assert.Equal(t,initialSprintID,tt.i.CommitedSprintID())
assert.Equal(t, tt.item.Status(), initialStatus)
}
roblaszczak
75
func (i *Item) CommitToSprint(s *sprint.Sprint) error {
if !i.IsScheduledForRelease() {
return ErrMustBeScheduledToCommit
}
if i.IsCommittedToSprint() {
i.UncommitFromSprint()
}
i.setStatus(ItemStatusCommited)
i.setSprintID(s.ID())
return nil
}
roblaszczak
76
func (i *Item) CommitToSprint(s *sprint.Sprint) error {
if !i.IsScheduledForRelease() {
return ErrMustBeScheduledToCommit
}
if i.IsCommittedToSprint() {
i.UncommitFromSprint()
}
i.setStatus(ItemStatusCommited)
i.setSprintID(s.ID())
return nil
}
roblaszczak
77
func (i *Item) CommitToSprint(s *sprint.Sprint) error {
if !i.IsScheduledForRelease() {
return ErrMustBeScheduledToCommit
}
if i.IsCommittedToSprint() {
i.UncommitFromSprint()
}
i.setStatus(ItemStatusCommited)
i.setSprintID(s.ID())
return nil
}
roblaszczak
78
func (i *Item) CommitToSprint(s *sprint.Sprint) error {
if !i.IsScheduledForRelease() {
return ErrMustBeScheduledToCommit
}
if i.IsCommittedToSprint() {
i.UncommitFromSprint()
}
i.setStatus(ItemStatusCommited)
i.setSprintID(s.ID())
return nil
}
roblaszczak
79
roblaszczak
80
func (i *Item) CommitToSprint(s *sprint.Sprint) error {
if !i.IsScheduledForRelease() {
return ErrMustBeScheduledToCommit
}
if i.IsCommittedToSprint() {
i.UncommitFromSprint()
}
i.setStatus(ItemStatusCommited)
i.setSprintID(s.ID())
return nil
}
roblaszczak
81
VALUE
OBJECT
roblaszczak
82
(RACZEJ)
NIE POSIADA ID
roblaszczak
83
JEST IMMUTABLE
roblaszczak
84
NIE POSIADA ZACHOWAŃ
roblaszczak
85
CZĘSTO ŁĄCZY 2
WARTOŚCI KTÓRE
ODDZIELNIE NIE MAJĄ
SENSU
roblaszczak
86
type Price struct {
cents int
currency string
}
roblaszczak
87
func NewPrice(cents int, currency string) (Price, error)
{
if cents <= 0 {
return Price{}, ErrCentsTooLow
}
if len(currency) != 3 {
return Price{}, ErrInvalidCurrency
}
return Price{cents, currency}, nil
}
roblaszczak
88
func NewPrice(cents int, currency string) (Price, error)
{
if cents <= 0 {
return Price{}, ErrCentsTooLow
}
if len(currency) != 3 {
return Price{}, ErrInvalidCurrency
}
return Price{cents, currency}, nil
}
roblaszczak
89
func NewPrice(cents int, currency string) (Price, error)
{
if cents <= 0 {
return Price{}, ErrCentsTooLow
}
if len(currency) != 3 {
return Price{}, ErrInvalidCurrency
}
return Price{cents, currency}, nil
}
roblaszczak
90
Price{}
roblaszczak
91
func (p Price) Empty() bool {
return p.cents == 0 || p.currency == ""
}
roblaszczak
92
func (p Price) Add(toAdd Price) (Price, error) {
if p.currency != toAdd.currency {
return Price{}, ErrCurrencyDoesntMatch
}
return NewPrice(p.cents+toAdd.cents, p.currency)
}
roblaszczak
93
REPOSITORY
roblaszczak
94
roblaszczak
95
roblaszczak
96
ODKŁADAMY DECYZJĘ
O BAZIE DANYCH
NA PÓŹNIEJ
roblaszczak
97
package backlog
type ItemRepository interface {
Add(*Item) error
ByID(ItemID) (*Item, error)
Update(*Item) error
}
roblaszczak
98
package backlog
// ….
var (
ErrItemNotFound = errors.New("item not found")
ErrItemAlreadyExists = errors.New("item already
exists")
)
roblaszczak
99
package backlog
type ItemRepository interface {
Add(*Item) error
ByID(ItemID) (*Item, error)
Update(*Item) error
}
roblaszczak
100
type BacklogItemRepository struct {
items map[string]backlog.Item
}
func (r *BacklogItemRepository) Add(i *backlog.Item)
error {
if _, ok := r.items[i.ID().String()]; ok {
return backlog.ErrItemAlreadyExists
}
r.items[i.ID().String()] = *i
return nil
}
roblaszczak
101
type BacklogItemRepository struct {
items map[string]backlog.Item
}
func (r *BacklogItemRepository) Add(i *backlog.Item)
error {
if _, ok := r.items[i.ID().String()]; ok {
return backlog.ErrItemAlreadyExists
}
r.items[i.ID().String()] = *i
return nil
}
roblaszczak
102
type BacklogItemRepository struct {
items map[string]backlog.Item
}
func (r *BacklogItemRepository) Add(i *backlog.Item)
error {
if _, ok := r.items[i.ID().String()]; ok {
return backlog.ErrItemAlreadyExists
}
r.items[i.ID().String()] = *i
return nil
}
roblaszczak
103
Jak tego teraz użyć?
roblaszczak
104
CQRS
roblaszczak
105
MAŁE SERWISY
APLIKACYJNE
PER USE CASE
roblaszczak
106
REUŻYWALNE
roblaszczak
107
roblaszczak
108
type CommitBacklogItemToSprint struct {
BacklogItemID backlog.ItemID
SprintID sprint.ID
}
roblaszczak
109
type CommitBacklogItemToSprint struct {
BacklogItemID backlog.ItemID
SprintID sprint.ID
}
roblaszczak
110
type CommitBacklogItemToSprintHandler struct {
sprintRepository sprint.Repository
backlogItemRepository backlog.ItemRepository
}
roblaszczak
111
func NewCommitToSprintHandler(
sprintRepository sprint.Repository,
backlogItemRepository backlog.ItemRepository,
) CommitBacklogItemToSprintHandler {
return CommitBacklogItemToSprintHandler{
sprintRepository,
backlogItemRepository,
}
}
roblaszczak
112
func NewCommitToSprintHandler(
sprintRepository sprint.Repository,
backlogItemRepository backlog.ItemRepository,
) CommitBacklogItemToSprintHandler {
return CommitBacklogItemToSprintHandler{
sprintRepository,
backlogItemRepository,
}
}
roblaszczak
113
type CommitBacklogItemToSprintHandler struct {
sprintRepository sprint.Repository
backlogItemRepository backlog.ItemRepository
}
roblaszczak
114
type CommitBacklogItemToSprintHandler struct {
sprintRepository sprint.Repository
backlogItemRepository backlog.ItemRepository
}
roblaszczak
115
type AddBacklogItem struct {
ID backlog.ItemID
Name string
}
func (a AddBacklogItemHandler) Handle(cmd AddBacklogItem)
error {
i, err := backlog.NewItem(cmd.ID, cmd.Name)
if err != nil {
return err
}
return a.backlogItemRepository.Add(i)
}
roblaszczak
116
type AddBacklogItem struct {
ID backlog.ItemID
Name string
}
func (a AddBacklogItemHandler) Handle(cmd AddBacklogItem)
error {
i, err := backlog.NewItem(cmd.ID, cmd.Name)
if err != nil {
return err
}
return a.backlogItemRepository.Add(i)
}
roblaszczak
117
type AddBacklogItem struct {
ID backlog.ItemID
Name string
}
func (a AddBacklogItemHandler) Handle(cmd AddBacklogItem)
error {
i, err := backlog.NewItem(cmd.ID, cmd.Name)
if err != nil {
return err
}
return a.backlogItemRepository.Add(i)
}
roblaszczak
118
type AddBacklogItem struct {
ID backlog.ItemID
Name string
}
func (a AddBacklogItemHandler) Handle(cmd AddBacklogItem)
error {
i, err := backlog.NewItem(cmd.ID, cmd.Name)
if err != nil {
return err
}
return a.backlogItemRepository.Add(i)
}
roblaszczak
119
type AddSprint struct {
// ...
roblaszczak
120
type CommitBacklogItemToSprint struct {
BacklogItemID backlog.ItemID
SprintID sprint.ID
}
roblaszczak
121
func (c CommitBacklogItemToSprintHandler) Handle(cmd
CommitBacklogItemToSprint) error {
sprint, err := c.sprintRepository.ByID(cmd.SprintID)
if err != nil {
return err
}
item, err :=
c.backlogItemRepository.ByID(cmd.BacklogItemID)
if err != nil {
return err
}
// ...
roblaszczak
122
func (c CommitBacklogItemToSprintHandler) Handle(cmd
CommitBacklogItemToSprint) error {
sprint, err := c.sprintRepository.ByID(cmd.SprintID)
if err != nil {
return err
}
item, err :=
c.backlogItemRepository.ByID(cmd.BacklogItemID)
if err != nil {
return err
}
// ...
roblaszczak
123
func (c CommitBacklogItemToSprintHandler) Handle(cmd
CommitBacklogItemToSprint) error {
// ...
if err := item.CommitToSprint(sprint); err != nil {
return err
}
return c.backlogItemRepository.Update(item)
}
roblaszczak
124
func (c CommitBacklogItemToSprintHandler) Handle(cmd
CommitBacklogItemToSprint) error {
// …
if item.IsCommitedToBacklog() {
return err
}
// …
return c.backlogItemRepository.Update(item)
}
roblaszczak
125
func (c CommitBacklogItemToSprintHandler) Handle(cmd
CommitBacklogItemToSprint) error {
// …
if item.IsCommitedToBacklog() {
return err
}
// …
return c.backlogItemRepository.Update(item)
}
roblaszczak
126
DOBRE MIEJSCE NA
CROSS-CUTTING
CONCERNS
roblaszczak
127
REST
roblaszczak
128
r.Get("/sprint/{sprintID}/backlog/item/{itemID}", func(w
http.ResponseWriter, r *http.Request) {
backlogItemID := id.FromStringMust(chi.URLParam(r, "itemID"))
sprintID := id.FromStringMust(chi.URLParam(r, "sprintID"))
err := h.Handle(command.CommitBacklogItemToSprint{
BacklogItemID: backlog.ItemID{backlogItemID},
SprintID: sprint.ID{sprintID},
})
if err != nil {
log.Println("error:", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
roblaszczak
129
r.Get("/sprint/{sprintID}/backlog/item/{itemID}", func(w
http.ResponseWriter, r *http.Request) {
backlogItemID := id.FromStringMust(chi.URLParam(r, "itemID"))
sprintID := id.FromStringMust(chi.URLParam(r, "sprintID"))
err := h.Handle(command.CommitBacklogItemToSprint{
BacklogItemID: backlog.ItemID{backlogItemID},
SprintID: sprint.ID{sprintID},
})
if err != nil {
log.Println("error:", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
roblaszczak
130
r.Get("/sprint/{sprintID}/backlog/item/{itemID}", func(w
http.ResponseWriter, r *http.Request) {
backlogItemID := id.FromStringMust(chi.URLParam(r, "itemID"))
sprintID := id.FromStringMust(chi.URLParam(r, "sprintID"))
err := h.Handle(command.CommitBacklogItemToSprint{
BacklogItemID: backlog.ItemID{backlogItemID},
SprintID: sprint.ID{sprintID},
})
if err != nil {
log.Println("error:", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
roblaszczak
131
Gdzie to
teraz
upchnąć?
roblaszczak
132
Jeden package
==
tracimy
enkapsulację
roblaszczak
133
.
├── app
│ └── command
├── domain
│ ├── backlog
│ └── sprint
├── infrastructure
│ └── memory
└── interfaces
└── rest
roblaszczak
134
.
├── app
│ └── command
├── domain
│ ├── backlog
│ └── sprint
├── infrastructure
│ └── memory
└── interfaces
└── rest
roblaszczak
135
.
├── app
│ └── command
├── domain
│ ├── backlog
│ └── sprint
├── infrastructure
│ └── memory
└── interfaces
└── rest
roblaszczak
136
.
├── app
│ └── command
├── domain
│ ├── backlog
│ └── sprint
├── infrastructure
│ └── memory
└── interfaces
└── rest
roblaszczak
137
.
├── app
│ └── command
├── domain
│ ├── backlog
│ └── sprint
├── infrastructure
│ └── memory
└── interfaces
└── rest
roblaszczak
138
domain
├── backlog
│ ├── item.go
│ ├── item_status.go
│ ├── repository.go
│ └── unmarshal.go
├── error.go
└── sprint
├── repository.go
└── sprint.go
roblaszczak
139
roblaszczak
140
github.com/roblaszczak/go-cleanarch
roblaszczak
141
github.com/roblaszczak/go-cleanarch
(wszystkie linki podam pod koniec prezentacji)
roblaszczak
142
roblaszczak
143
roblaszczak
144
type Item struct {
id ItemID `json:"id"`
name string `json:"name"`
status ItemStatus `json:"status"`
releaseSchedule time.Time `json:"release_schedule"`
commitedSprintID sprint.ID `json:"commited_sprint_id"`
}
roblaszczak
145
Zmiana widoku
wymaga zmiany w
domenie
roblaszczak
146
Atrybuty są
prywatne więc się
nie zmarshalują :)
roblaszczak
147
type BacklogItemView struct {
ID string `json:"id"`
Name string `json:"name"`
}
func NewBacklogItemView(item *backlog.Item)
BacklogItemView {
return BacklogItemView{item.ID().String(), item.Name()}
}
roblaszczak
148
SOLID Motivational Posters by Derick Bailey, used under CC BY-SA 3.0 US
roblaszczak
149
PERSYSTENTNE
REPOZYTORIUM
roblaszczak
150
roblaszczak
151
roblaszczak
152
Jak unmarshalować
obiekt domenowy bez
zbytniej ingerencji?
roblaszczak
153type ItemUnmarshalData struct {
ID ItemID
Name string
Status ItemStatus
ReleaseSchedule time.Time
CommitedSprintID sprint.ID
}
func UnmarshalItem(ud ItemUnmarshalData) *Item {
return &Item{
ud.ID,
ud.Name,
ud.Status,
ud.ReleaseSchedule,
ud.CommitedSprintID,
}
}
roblaszczak
154type ItemUnmarshalData struct {
ID ItemID
Name string
Status ItemStatus
ReleaseSchedule time.Time
CommitedSprintID sprint.ID
}
func UnmarshalItem(ud ItemUnmarshalData) *Item {
return &Item{
ud.ID,
ud.Name,
ud.Status,
ud.ReleaseSchedule,
ud.CommitedSprintID,
}
}
roblaszczak
155
ANEMICZNY
MODEL
roblaszczak
156
type User struct {
id ID
email string
name string
address string
}
func (u *User) Id() ID { return u.id }
func (u *User) SetId(id ID) { u.id = id }
func (u *User) Email() string { return u.email }
func (u *User) SetEmail(email string) { u.email = email }
func (u *User) Name() string { return u.name }
func (u *User) SetName(name string) { u.name = name }
func (u *User) Address() string { return u.address }
func (u *User) SetAddress(address string) { u.address = address }
roblaszczak
157
SILVER BULLET
NIE ISTNIEJE
DDD TEGO NIE ZMIENI
roblaszczak
158
CRUD IS OK
roblaszczak
159
LITE DDD =
roblaszczak
160
What next?
roblaszczak
161
Wrzucę linki na Twitterze
roblaszczak
roblaszczak
162
roblaszczak
163
roblaszczak
164
roblaszczak
165
https://threedots.tech/
roblaszczak
166
roblaszczak
Three Dots Labs
hire us!
roblaszczak
github.com/ThreeDotsLabs/watermill/
roblaszczak
DZIĘKI!
roblaszczak
robert@threedotslabs.com

Mais conteúdo relacionado

Mais procurados

Clojure 1.1 And Beyond
Clojure 1.1 And BeyondClojure 1.1 And Beyond
Clojure 1.1 And BeyondMike Fogus
 
How to Clone Flappy Bird in Swift
How to Clone Flappy Bird in SwiftHow to Clone Flappy Bird in Swift
How to Clone Flappy Bird in SwiftGiordano Scalzo
 
The Ring programming language version 1.5.2 book - Part 78 of 181
The Ring programming language version 1.5.2 book - Part 78 of 181The Ring programming language version 1.5.2 book - Part 78 of 181
The Ring programming language version 1.5.2 book - Part 78 of 181Mahmoud Samir Fayed
 
AST Transformations at JFokus
AST Transformations at JFokusAST Transformations at JFokus
AST Transformations at JFokusHamletDRC
 
The Ring programming language version 1.2 book - Part 79 of 84
The Ring programming language version 1.2 book - Part 79 of 84The Ring programming language version 1.2 book - Part 79 of 84
The Ring programming language version 1.2 book - Part 79 of 84Mahmoud Samir Fayed
 
The Ring programming language version 1.8 book - Part 91 of 202
The Ring programming language version 1.8 book - Part 91 of 202The Ring programming language version 1.8 book - Part 91 of 202
The Ring programming language version 1.8 book - Part 91 of 202Mahmoud Samir Fayed
 
Javascript Primer
Javascript PrimerJavascript Primer
Javascript PrimerAdam Hepton
 
Promise: async programming hero
Promise: async programming heroPromise: async programming hero
Promise: async programming heroThe Software House
 
Introduction to modern c++ principles(part 1)
Introduction to modern c++ principles(part 1)Introduction to modern c++ principles(part 1)
Introduction to modern c++ principles(part 1)Oky Firmansyah
 
The mighty js_function
The mighty js_functionThe mighty js_function
The mighty js_functiontimotheeg
 
Lisp Macros in 20 Minutes (Featuring Clojure)
Lisp Macros in 20 Minutes (Featuring Clojure)Lisp Macros in 20 Minutes (Featuring Clojure)
Lisp Macros in 20 Minutes (Featuring Clojure)Phil Calçado
 
What We Talk About When We Talk About Unit Testing
What We Talk About When We Talk About Unit TestingWhat We Talk About When We Talk About Unit Testing
What We Talk About When We Talk About Unit TestingKevlin Henney
 
ConFess Vienna 2015 - Metaprogramming with Groovy
ConFess Vienna 2015 - Metaprogramming with GroovyConFess Vienna 2015 - Metaprogramming with Groovy
ConFess Vienna 2015 - Metaprogramming with GroovyIván López Martín
 
Test-driven JavaScript Development - OPITZ CONSULTING - Tobias Bosch - Stefa...
Test-driven JavaScript Development - OPITZ CONSULTING -  Tobias Bosch - Stefa...Test-driven JavaScript Development - OPITZ CONSULTING -  Tobias Bosch - Stefa...
Test-driven JavaScript Development - OPITZ CONSULTING - Tobias Bosch - Stefa...OPITZ CONSULTING Deutschland
 
Java 7 JUG Summer Camp
Java 7 JUG Summer CampJava 7 JUG Summer Camp
Java 7 JUG Summer Campjulien.ponge
 

Mais procurados (20)

Clojure 1.1 And Beyond
Clojure 1.1 And BeyondClojure 1.1 And Beyond
Clojure 1.1 And Beyond
 
How to Clone Flappy Bird in Swift
How to Clone Flappy Bird in SwiftHow to Clone Flappy Bird in Swift
How to Clone Flappy Bird in Swift
 
The Ring programming language version 1.5.2 book - Part 78 of 181
The Ring programming language version 1.5.2 book - Part 78 of 181The Ring programming language version 1.5.2 book - Part 78 of 181
The Ring programming language version 1.5.2 book - Part 78 of 181
 
AST Transformations at JFokus
AST Transformations at JFokusAST Transformations at JFokus
AST Transformations at JFokus
 
The Ring programming language version 1.2 book - Part 79 of 84
The Ring programming language version 1.2 book - Part 79 of 84The Ring programming language version 1.2 book - Part 79 of 84
The Ring programming language version 1.2 book - Part 79 of 84
 
The Ring programming language version 1.8 book - Part 91 of 202
The Ring programming language version 1.8 book - Part 91 of 202The Ring programming language version 1.8 book - Part 91 of 202
The Ring programming language version 1.8 book - Part 91 of 202
 
Javascript Primer
Javascript PrimerJavascript Primer
Javascript Primer
 
JavaScript Primer
JavaScript PrimerJavaScript Primer
JavaScript Primer
 
Promise: async programming hero
Promise: async programming heroPromise: async programming hero
Promise: async programming hero
 
JavaScript Patterns
JavaScript PatternsJavaScript Patterns
JavaScript Patterns
 
Introduction to modern c++ principles(part 1)
Introduction to modern c++ principles(part 1)Introduction to modern c++ principles(part 1)
Introduction to modern c++ principles(part 1)
 
JavaScript Primer
JavaScript PrimerJavaScript Primer
JavaScript Primer
 
The mighty js_function
The mighty js_functionThe mighty js_function
The mighty js_function
 
Lisp Macros in 20 Minutes (Featuring Clojure)
Lisp Macros in 20 Minutes (Featuring Clojure)Lisp Macros in 20 Minutes (Featuring Clojure)
Lisp Macros in 20 Minutes (Featuring Clojure)
 
Groovy
GroovyGroovy
Groovy
 
What We Talk About When We Talk About Unit Testing
What We Talk About When We Talk About Unit TestingWhat We Talk About When We Talk About Unit Testing
What We Talk About When We Talk About Unit Testing
 
Functional C++
Functional C++Functional C++
Functional C++
 
ConFess Vienna 2015 - Metaprogramming with Groovy
ConFess Vienna 2015 - Metaprogramming with GroovyConFess Vienna 2015 - Metaprogramming with Groovy
ConFess Vienna 2015 - Metaprogramming with Groovy
 
Test-driven JavaScript Development - OPITZ CONSULTING - Tobias Bosch - Stefa...
Test-driven JavaScript Development - OPITZ CONSULTING -  Tobias Bosch - Stefa...Test-driven JavaScript Development - OPITZ CONSULTING -  Tobias Bosch - Stefa...
Test-driven JavaScript Development - OPITZ CONSULTING - Tobias Bosch - Stefa...
 
Java 7 JUG Summer Camp
Java 7 JUG Summer CampJava 7 JUG Summer Camp
Java 7 JUG Summer Camp
 

Semelhante a Tactical DDD patterns in Go

Andriy Slobodyanyk "How to Use Hibernate: Key Problems and Solutions"
Andriy Slobodyanyk "How to Use Hibernate: Key Problems and Solutions"Andriy Slobodyanyk "How to Use Hibernate: Key Problems and Solutions"
Andriy Slobodyanyk "How to Use Hibernate: Key Problems and Solutions"LogeekNightUkraine
 
JavaScript - i och utanför webbläsaren (2010-03-03)
JavaScript - i och utanför webbläsaren (2010-03-03)JavaScript - i och utanför webbläsaren (2010-03-03)
JavaScript - i och utanför webbläsaren (2010-03-03)Anders Jönsson
 
Empathic Programming - How to write comprehensible code
Empathic Programming - How to write comprehensible codeEmpathic Programming - How to write comprehensible code
Empathic Programming - How to write comprehensible codeMario Gleichmann
 
Ugly code
Ugly codeUgly code
Ugly codeOdd-e
 
Tips and Tricks of Developing .NET Application
Tips and Tricks of Developing .NET ApplicationTips and Tricks of Developing .NET Application
Tips and Tricks of Developing .NET ApplicationJoni
 
Самые вкусные баги из игрового кода: как ошибаются наши коллеги-программисты ...
Самые вкусные баги из игрового кода: как ошибаются наши коллеги-программисты ...Самые вкусные баги из игрового кода: как ошибаются наши коллеги-программисты ...
Самые вкусные баги из игрового кода: как ошибаются наши коллеги-программисты ...DevGAMM Conference
 
CodiLime Tech Talk - Grzegorz Rozdzialik: What the java script
CodiLime Tech Talk - Grzegorz Rozdzialik: What the java scriptCodiLime Tech Talk - Grzegorz Rozdzialik: What the java script
CodiLime Tech Talk - Grzegorz Rozdzialik: What the java scriptCodiLime
 
Node Anti-Patterns and Bad Practices
Node Anti-Patterns and Bad PracticesNode Anti-Patterns and Bad Practices
Node Anti-Patterns and Bad PracticesPedro Teixeira
 
SWP - A Generic Language Parser
SWP - A Generic Language ParserSWP - A Generic Language Parser
SWP - A Generic Language Parserkamaelian
 
Parsing with Perl6 Grammars
Parsing with Perl6 GrammarsParsing with Perl6 Grammars
Parsing with Perl6 Grammarsabrummett
 
Lo Mejor Del Pdc2008 El Futrode C#
Lo Mejor Del Pdc2008 El Futrode C#Lo Mejor Del Pdc2008 El Futrode C#
Lo Mejor Del Pdc2008 El Futrode C#Juan Pablo
 
GoCracow #5 Bartlomiej klimczak - GoBDD
GoCracow #5 Bartlomiej klimczak - GoBDDGoCracow #5 Bartlomiej klimczak - GoBDD
GoCracow #5 Bartlomiej klimczak - GoBDDBartłomiej Kiełbasa
 
Why Sifu?
Why Sifu?Why Sifu?
Why Sifu?Sifu
 
C# 6 and 7 and Futures 20180607
C# 6 and 7 and Futures 20180607C# 6 and 7 and Futures 20180607
C# 6 and 7 and Futures 20180607Kevin Hazzard
 
Intel JIT Talk
Intel JIT TalkIntel JIT Talk
Intel JIT Talkiamdvander
 

Semelhante a Tactical DDD patterns in Go (20)

Andriy Slobodyanyk "How to Use Hibernate: Key Problems and Solutions"
Andriy Slobodyanyk "How to Use Hibernate: Key Problems and Solutions"Andriy Slobodyanyk "How to Use Hibernate: Key Problems and Solutions"
Andriy Slobodyanyk "How to Use Hibernate: Key Problems and Solutions"
 
JavaScript - i och utanför webbläsaren (2010-03-03)
JavaScript - i och utanför webbläsaren (2010-03-03)JavaScript - i och utanför webbläsaren (2010-03-03)
JavaScript - i och utanför webbläsaren (2010-03-03)
 
Empathic Programming - How to write comprehensible code
Empathic Programming - How to write comprehensible codeEmpathic Programming - How to write comprehensible code
Empathic Programming - How to write comprehensible code
 
Ugly code
Ugly codeUgly code
Ugly code
 
Tips and Tricks of Developing .NET Application
Tips and Tricks of Developing .NET ApplicationTips and Tricks of Developing .NET Application
Tips and Tricks of Developing .NET Application
 
Самые вкусные баги из игрового кода: как ошибаются наши коллеги-программисты ...
Самые вкусные баги из игрового кода: как ошибаются наши коллеги-программисты ...Самые вкусные баги из игрового кода: как ошибаются наши коллеги-программисты ...
Самые вкусные баги из игрового кода: как ошибаются наши коллеги-программисты ...
 
CodiLime Tech Talk - Grzegorz Rozdzialik: What the java script
CodiLime Tech Talk - Grzegorz Rozdzialik: What the java scriptCodiLime Tech Talk - Grzegorz Rozdzialik: What the java script
CodiLime Tech Talk - Grzegorz Rozdzialik: What the java script
 
Node Anti-Patterns and Bad Practices
Node Anti-Patterns and Bad PracticesNode Anti-Patterns and Bad Practices
Node Anti-Patterns and Bad Practices
 
SWP - A Generic Language Parser
SWP - A Generic Language ParserSWP - A Generic Language Parser
SWP - A Generic Language Parser
 
Parsing with Perl6 Grammars
Parsing with Perl6 GrammarsParsing with Perl6 Grammars
Parsing with Perl6 Grammars
 
Lo Mejor Del Pdc2008 El Futrode C#
Lo Mejor Del Pdc2008 El Futrode C#Lo Mejor Del Pdc2008 El Futrode C#
Lo Mejor Del Pdc2008 El Futrode C#
 
Elegant objects
Elegant objectsElegant objects
Elegant objects
 
GoCracow #5 Bartlomiej klimczak - GoBDD
GoCracow #5 Bartlomiej klimczak - GoBDDGoCracow #5 Bartlomiej klimczak - GoBDD
GoCracow #5 Bartlomiej klimczak - GoBDD
 
mobl
moblmobl
mobl
 
Why Sifu
Why SifuWhy Sifu
Why Sifu
 
Why Sifu?
Why Sifu?Why Sifu?
Why Sifu?
 
Scala @ TomTom
Scala @ TomTomScala @ TomTom
Scala @ TomTom
 
Elm: give it a try
Elm: give it a tryElm: give it a try
Elm: give it a try
 
C# 6 and 7 and Futures 20180607
C# 6 and 7 and Futures 20180607C# 6 and 7 and Futures 20180607
C# 6 and 7 and Futures 20180607
 
Intel JIT Talk
Intel JIT TalkIntel JIT Talk
Intel JIT Talk
 

Último

UI5ers live - Custom Controls wrapping 3rd-party libs.pptx
UI5ers live - Custom Controls wrapping 3rd-party libs.pptxUI5ers live - Custom Controls wrapping 3rd-party libs.pptx
UI5ers live - Custom Controls wrapping 3rd-party libs.pptxAndreas Kunz
 
OpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full Recording
OpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full RecordingOpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full Recording
OpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full RecordingShane Coughlan
 
Keeping your build tool updated in a multi repository world
Keeping your build tool updated in a multi repository worldKeeping your build tool updated in a multi repository world
Keeping your build tool updated in a multi repository worldRoberto Pérez Alcolea
 
Odoo 14 - eLearning Module In Odoo 14 Enterprise
Odoo 14 - eLearning Module In Odoo 14 EnterpriseOdoo 14 - eLearning Module In Odoo 14 Enterprise
Odoo 14 - eLearning Module In Odoo 14 Enterprisepreethippts
 
Sending Calendar Invites on SES and Calendarsnack.pdf
Sending Calendar Invites on SES and Calendarsnack.pdfSending Calendar Invites on SES and Calendarsnack.pdf
Sending Calendar Invites on SES and Calendarsnack.pdf31events.com
 
Real-time Tracking and Monitoring with Cargo Cloud Solutions.pptx
Real-time Tracking and Monitoring with Cargo Cloud Solutions.pptxReal-time Tracking and Monitoring with Cargo Cloud Solutions.pptx
Real-time Tracking and Monitoring with Cargo Cloud Solutions.pptxRTS corp
 
The Role of IoT and Sensor Technology in Cargo Cloud Solutions.pptx
The Role of IoT and Sensor Technology in Cargo Cloud Solutions.pptxThe Role of IoT and Sensor Technology in Cargo Cloud Solutions.pptx
The Role of IoT and Sensor Technology in Cargo Cloud Solutions.pptxRTS corp
 
2024 DevNexus Patterns for Resiliency: Shuffle shards
2024 DevNexus Patterns for Resiliency: Shuffle shards2024 DevNexus Patterns for Resiliency: Shuffle shards
2024 DevNexus Patterns for Resiliency: Shuffle shardsChristopher Curtin
 
SAM Training Session - How to use EXCEL ?
SAM Training Session - How to use EXCEL ?SAM Training Session - How to use EXCEL ?
SAM Training Session - How to use EXCEL ?Alexandre Beguel
 
VK Business Profile - provides IT solutions and Web Development
VK Business Profile - provides IT solutions and Web DevelopmentVK Business Profile - provides IT solutions and Web Development
VK Business Profile - provides IT solutions and Web Developmentvyaparkranti
 
Alfresco TTL#157 - Troubleshooting Made Easy: Deciphering Alfresco mTLS Confi...
Alfresco TTL#157 - Troubleshooting Made Easy: Deciphering Alfresco mTLS Confi...Alfresco TTL#157 - Troubleshooting Made Easy: Deciphering Alfresco mTLS Confi...
Alfresco TTL#157 - Troubleshooting Made Easy: Deciphering Alfresco mTLS Confi...Angel Borroy López
 
Post Quantum Cryptography – The Impact on Identity
Post Quantum Cryptography – The Impact on IdentityPost Quantum Cryptography – The Impact on Identity
Post Quantum Cryptography – The Impact on Identityteam-WIBU
 
eSoftTools IMAP Backup Software and migration tools
eSoftTools IMAP Backup Software and migration toolseSoftTools IMAP Backup Software and migration tools
eSoftTools IMAP Backup Software and migration toolsosttopstonverter
 
Effectively Troubleshoot 9 Types of OutOfMemoryError
Effectively Troubleshoot 9 Types of OutOfMemoryErrorEffectively Troubleshoot 9 Types of OutOfMemoryError
Effectively Troubleshoot 9 Types of OutOfMemoryErrorTier1 app
 
Exploring Selenium_Appium Frameworks for Seamless Integration with HeadSpin.pdf
Exploring Selenium_Appium Frameworks for Seamless Integration with HeadSpin.pdfExploring Selenium_Appium Frameworks for Seamless Integration with HeadSpin.pdf
Exploring Selenium_Appium Frameworks for Seamless Integration with HeadSpin.pdfkalichargn70th171
 
A healthy diet for your Java application Devoxx France.pdf
A healthy diet for your Java application Devoxx France.pdfA healthy diet for your Java application Devoxx France.pdf
A healthy diet for your Java application Devoxx France.pdfMarharyta Nedzelska
 
Osi security architecture in network.pptx
Osi security architecture in network.pptxOsi security architecture in network.pptx
Osi security architecture in network.pptxVinzoCenzo
 
Ronisha Informatics Private Limited Catalogue
Ronisha Informatics Private Limited CatalogueRonisha Informatics Private Limited Catalogue
Ronisha Informatics Private Limited Catalogueitservices996
 
Enhancing Supply Chain Visibility with Cargo Cloud Solutions.pdf
Enhancing Supply Chain Visibility with Cargo Cloud Solutions.pdfEnhancing Supply Chain Visibility with Cargo Cloud Solutions.pdf
Enhancing Supply Chain Visibility with Cargo Cloud Solutions.pdfRTS corp
 
Tech Tuesday - Mastering Time Management Unlock the Power of OnePlan's Timesh...
Tech Tuesday - Mastering Time Management Unlock the Power of OnePlan's Timesh...Tech Tuesday - Mastering Time Management Unlock the Power of OnePlan's Timesh...
Tech Tuesday - Mastering Time Management Unlock the Power of OnePlan's Timesh...OnePlan Solutions
 

Último (20)

UI5ers live - Custom Controls wrapping 3rd-party libs.pptx
UI5ers live - Custom Controls wrapping 3rd-party libs.pptxUI5ers live - Custom Controls wrapping 3rd-party libs.pptx
UI5ers live - Custom Controls wrapping 3rd-party libs.pptx
 
OpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full Recording
OpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full RecordingOpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full Recording
OpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full Recording
 
Keeping your build tool updated in a multi repository world
Keeping your build tool updated in a multi repository worldKeeping your build tool updated in a multi repository world
Keeping your build tool updated in a multi repository world
 
Odoo 14 - eLearning Module In Odoo 14 Enterprise
Odoo 14 - eLearning Module In Odoo 14 EnterpriseOdoo 14 - eLearning Module In Odoo 14 Enterprise
Odoo 14 - eLearning Module In Odoo 14 Enterprise
 
Sending Calendar Invites on SES and Calendarsnack.pdf
Sending Calendar Invites on SES and Calendarsnack.pdfSending Calendar Invites on SES and Calendarsnack.pdf
Sending Calendar Invites on SES and Calendarsnack.pdf
 
Real-time Tracking and Monitoring with Cargo Cloud Solutions.pptx
Real-time Tracking and Monitoring with Cargo Cloud Solutions.pptxReal-time Tracking and Monitoring with Cargo Cloud Solutions.pptx
Real-time Tracking and Monitoring with Cargo Cloud Solutions.pptx
 
The Role of IoT and Sensor Technology in Cargo Cloud Solutions.pptx
The Role of IoT and Sensor Technology in Cargo Cloud Solutions.pptxThe Role of IoT and Sensor Technology in Cargo Cloud Solutions.pptx
The Role of IoT and Sensor Technology in Cargo Cloud Solutions.pptx
 
2024 DevNexus Patterns for Resiliency: Shuffle shards
2024 DevNexus Patterns for Resiliency: Shuffle shards2024 DevNexus Patterns for Resiliency: Shuffle shards
2024 DevNexus Patterns for Resiliency: Shuffle shards
 
SAM Training Session - How to use EXCEL ?
SAM Training Session - How to use EXCEL ?SAM Training Session - How to use EXCEL ?
SAM Training Session - How to use EXCEL ?
 
VK Business Profile - provides IT solutions and Web Development
VK Business Profile - provides IT solutions and Web DevelopmentVK Business Profile - provides IT solutions and Web Development
VK Business Profile - provides IT solutions and Web Development
 
Alfresco TTL#157 - Troubleshooting Made Easy: Deciphering Alfresco mTLS Confi...
Alfresco TTL#157 - Troubleshooting Made Easy: Deciphering Alfresco mTLS Confi...Alfresco TTL#157 - Troubleshooting Made Easy: Deciphering Alfresco mTLS Confi...
Alfresco TTL#157 - Troubleshooting Made Easy: Deciphering Alfresco mTLS Confi...
 
Post Quantum Cryptography – The Impact on Identity
Post Quantum Cryptography – The Impact on IdentityPost Quantum Cryptography – The Impact on Identity
Post Quantum Cryptography – The Impact on Identity
 
eSoftTools IMAP Backup Software and migration tools
eSoftTools IMAP Backup Software and migration toolseSoftTools IMAP Backup Software and migration tools
eSoftTools IMAP Backup Software and migration tools
 
Effectively Troubleshoot 9 Types of OutOfMemoryError
Effectively Troubleshoot 9 Types of OutOfMemoryErrorEffectively Troubleshoot 9 Types of OutOfMemoryError
Effectively Troubleshoot 9 Types of OutOfMemoryError
 
Exploring Selenium_Appium Frameworks for Seamless Integration with HeadSpin.pdf
Exploring Selenium_Appium Frameworks for Seamless Integration with HeadSpin.pdfExploring Selenium_Appium Frameworks for Seamless Integration with HeadSpin.pdf
Exploring Selenium_Appium Frameworks for Seamless Integration with HeadSpin.pdf
 
A healthy diet for your Java application Devoxx France.pdf
A healthy diet for your Java application Devoxx France.pdfA healthy diet for your Java application Devoxx France.pdf
A healthy diet for your Java application Devoxx France.pdf
 
Osi security architecture in network.pptx
Osi security architecture in network.pptxOsi security architecture in network.pptx
Osi security architecture in network.pptx
 
Ronisha Informatics Private Limited Catalogue
Ronisha Informatics Private Limited CatalogueRonisha Informatics Private Limited Catalogue
Ronisha Informatics Private Limited Catalogue
 
Enhancing Supply Chain Visibility with Cargo Cloud Solutions.pdf
Enhancing Supply Chain Visibility with Cargo Cloud Solutions.pdfEnhancing Supply Chain Visibility with Cargo Cloud Solutions.pdf
Enhancing Supply Chain Visibility with Cargo Cloud Solutions.pdf
 
Tech Tuesday - Mastering Time Management Unlock the Power of OnePlan's Timesh...
Tech Tuesday - Mastering Time Management Unlock the Power of OnePlan's Timesh...Tech Tuesday - Mastering Time Management Unlock the Power of OnePlan's Timesh...
Tech Tuesday - Mastering Time Management Unlock the Power of OnePlan's Timesh...
 

Tactical DDD patterns in Go