Making Decisions
A decider connects a state with the decision made on it.
To create one, call the NewDecider function with the state and a function that receives the command and the current state, and returns the events to write:
var ErrBookAlreadyAcquired = architecturekit.NewDomainError("book has already been acquired")
var acquireBook = architecturekit.NewDecider(bookState,
func(ctx context.Context, cmd AcquireBook, book Book) ([]architecturekit.Event, error) {
if book.IsAcquired {
return nil, fmt.Errorf("%w: %s", ErrBookAlreadyAcquired, cmd.BookID)
}
return []architecturekit.Event{
BookAcquired{
Title: cmd.Title,
Author: cmd.Author,
ISBN: cmd.ISBN,
},
}, nil
})
The types of the command and the state are taken from the function, so they do not have to be repeated. NewDecider returns a Decider. Its State function returns the state, and its Decide function decides on a command, given the state, with the function you handed over.
Note that calling NewDecider with nil as the state or as the function panics. A Decider that was declared but never created with NewDecider has neither, so calling its Decide function, or handing it to Execute, to the test fixture, or to Route panics as well, and Handle fails with an error (see Handling Commands over HTTP). Either way, the text names the mistake.
To reject a command, return an error created with the NewDomainError function. It takes a format string and arguments, like fmt.Errorf, and returns an error of the type *DomainError, whose message is exactly the formatted text, and which belongs to the category ErrDomain (see Handling Errors).
To let a caller or a test tell a rejection apart from the others, create it once, as a variable such as ErrBookAlreadyAcquired, and return it, or wrap it with fmt.Errorf and %w to add details, such as the ID of the book. errors.Is then finds the variable in the error, as well as the category ErrDomain (see Expecting Rejections).
A decider may check several rules:
var borrowBook = architecturekit.NewDecider(bookState,
func(ctx context.Context, cmd BorrowBook, book Book) ([]architecturekit.Event, error) {
if !book.IsAcquired {
return nil, architecturekit.NewDomainError("book %s does not exist", cmd.BookID)
}
if book.IsBorrowed {
return nil, architecturekit.NewDomainError("book %s is already borrowed", cmd.BookID)
}
return []architecturekit.Event{
BookBorrowed{
BorrowedBy: cmd.ReaderID,
BorrowedUntil: cmd.BorrowedUntil,
},
}, nil
})
If there is nothing to do, return neither events nor an error:
var returnBook = architecturekit.NewDecider(bookState,
func(ctx context.Context, cmd ReturnBook, book Book) ([]architecturekit.Event, error) {
if !book.IsBorrowed {
return nil, nil
}
return []architecturekit.Event{
BookReturned{},
}, nil
})