NHS National Programme for IT (2011)
Shaping the problem
Implementing the code
Notification (ext)
Billing (ext)
Customer
Product
Order
CS - Admin
Generic subdomain
Core subdomain
Support subdomain
Pricing
package main
import "github.com/company/email"
func main(){
aEmail, err := email.NewAdminEmail("info@whatever.com")
//...
cEmail, err := email.NewCustomerEmail("name.surname@gmail.com")
//...
}
// email.go
type Email string
func NewAdminEmail(s string) (Email, error) {
// validation logic here
}
func NewCustomerEmail(s string) (Email, error) {
// validation logic here
}
package main
import (
"github.com/company/admin"
"github.com/company/customer"
)
func main(){
aEmail, err := admin.NewEmail("info@whatever.com")
//...
cEmail, err := customer.NewEmail("name.surname@gmail.com")
//...
}
// customer/email.go
type Email string
func NewEmail(s string) (Email, error) {
// validation logic here
}
// admin/email.go
type Email string
func NewEmail(s string) (Email, error) {
// validation logic here
}
Notification (ext)
Billing (ext)
Customer
Product
Generic subdomain
Core subdomain
Support subdomain
Order
CS - Admin
Pricing
A
B
A
B
Downstream
upstream
A
B
A
B
customer-supplier
cooperate
separate ways
Customer
Product
📂 company
┣ 📂 customer
┃ ┗ 📄 customer.go
┣ 📂 product
┃ ┗ 📄 product.go
┣ 📄 go.mod
┗ 📄 go.sum
📂 customer-svc
┣ 📄 customer.go
┣ 📄 go.mod
┗ 📄 go.sum
//
📂 product-svc
┣ 📄 product.go
┣ 📄 go.mod
┗ 📄 go.sum
Pricing
Product
📂 company
┣ 📄 pricing.go
┣ 📄 product.go
┣ 📄 go.mod
┗ 📄 go.sum
Order
CS-Admin
Downstream
upstream
📂 company
┣ 📂 order
┃ ┗ 📄 order.go
┃ ┗ 📂 admin
┃ ┗ 📄 admin.go
┣ 📄 go.mod
┗ 📄 go.sum
📂 company
┣ 📂 order
┃ ┗ 📄 order.go
┣ 📂 admin
┃ ┗ 📄 admin.go // imports company/order
┣ 📄 go.mod
┗ 📄 go.sum
📂 order-svc
┣ 📄 order.go
┣ 📄 go.mod
┗ 📄 go.sum
//
📂 admin-svc
┣ 📄 admin.go
┣ 📄 order.go
┣ 📂 acl
┃ ┗ 📄 acl.go
┣ 📄 go.mod
┗ 📄 go.sum
package order
import (
"errors"
)
type emailInterface interface {
GetEmailValue() string
}
type email struct {
value string
}
func NewEmailAddress(address string) (emailInterface, error) {
if !isValidEmail(address) {
return nil, errors.New("Invalid email address")
}
return &email{value: address}, nil
}
func (e *email) String() string {
return e.value
}
func (e *email) GetEmailValue() string {
return e.value
}
func isValidEmail(address string) bool {
// validate email address format
return true
}
package order
import (
"errors"
"fmt"
"regexp"
"strings"
)
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$`)
var (
ErrEmail = errors.New("invalid email")
ErrEmailFormat = fmt.Errorf("%w: not a valid format", ErrEmail)
)
// Email represents an email
type Email string
// NewEmail returns a new Email
func NewEmail(s string) (Email, error) {
s = strings.TrimSpace(s)
if !emailRegex.MatchString(s) {
return "", ErrEmailFormat
}
return Email(s), nil
}
// String returns Email as string
func (s Email) String() string {
return string(s)
}
package order
import (
"time"
)
type Order struct {
ID string
Number string
Status string
PlacedBy string
PlacedAt time.Time
ShippedAt time.Time
DeliveredAt time.Time
}
func New(
id string,
number string,
status string,
placedBy string,
placedAt time.Time,
shippedAt time.Time,
deliveredAt time.Time,
) *Order {
return &Order{
ID: id,
Number: number,
Status: status,
PlacedBy: placedBy,
PlacedAt: placedAt,
ShippedAt: shippedAt,
DeliveredAt: deliveredAt,
}
}
package order
import (
"time"
)
type OrderManager struct{
// dependencies ...
}
func (mngr *OrderManager) MarkAsShipped(
o *Order,
shippedAt time.Time,
) {
o.Status = "shipped"
o.ShippedAt = shippedAt
}
func (mngr *OrderManager) MarkAsDelivered(
o *Order,
deliveredAt time.Time,
) {
o.Status = "Delivered"
o.DeliveredAt = deliveredAt
}
package order
import (
"errors"
"fmt"
"time"
)
// Domain errors raised by the order aggregate
var (
ErrNotShipped = errors.New("could not mark the order as shipped")
ErrNotDelivered = errors.New("could not mark the order as delivered")
)
// Order represents an order aggregate
type Order struct {
ID ID
Number Number
Status Status
PlacedBy UserID
PlacedAt time.Time
ShippedAt time.Time
DeliveredAt time.Time
}
// Place places a new order
// It is a factory function that uses the ubiquitous language of the domain
func Place(number Number, placedBy UserID) *Order {
return &Order{
ID: NewID(),
Number: number,
Status: Placed,
PlacedBy: placedBy,
PlacedAt: time.Now(),
}
}
// MarkAsShipped marks an order as shipped
// It returns ErrNotShipped when the operation violated the domain invariants
func (o *Order) MarkAsShipped() error {
switch {
case o.PlacedAt.IsZero():
return fmt.Errorf("%w: not placed", ErrNotShipped)
case o.Status == Shipped:
return fmt.Errorf("%w: already shipped", ErrNotShipped)
case o.Status == Delivered:
return fmt.Errorf("%w: already delivered", ErrNotShipped)
// more logic here if needed
}
o.Status = Shipped
o.ShippedAt = time.Now()
return nil
}
// MarkAsDelivered marks an order as delivered
// It returns ErrNotDelivered when the operation violated the domain invariants
func (o *Order) MarkAsDelivered() error {
switch {
case o.ShippedAt.IsZero():
return fmt.Errorf("%w: not shipped", ErrNotDelivered)
case o.Status == Delivered:
return fmt.Errorf("%w: already delivered", ErrNotDelivered)
}
o.Status = Delivered
o.DeliveredAt = time.Now()
return nil
}
package repository
import (
"context"
"errors"
"github.com/company/order"
)
type Order {
// some fields
}
type OrderOption func(*Order)
func WithCustomerID(customerID int) OrderOption {
return func(o *Order) {
o.CustomerID = customerID
}
}
type OrderRepository struct {
db *sql.DB
}
func (r *OrderRepository) Get(id string) (order.Order, error) {
row := r.db.QueryRow("SELECT * FROM orders WHERE id = ?", id)
var o Order
err := row.Scan(&o.ID /** more data **/)
if err != nil {
return nil, err
}
return mapOrderToDomainOrder(o)
}
func (r *OrderRepository) CreateOrder(opts ...OrderOption) error {
order := &Order{}
for _, opt := range opts {
opt(order)
}
query := "INSERT INTO orders ("
values := []interface{}{}
if order.CustomerID != 0 {
query += "customer_id, "
values = append(values, order.CustomerID)
}
// Remove the trailing comma and space.
query = query[:len(query)-2]
query += ") VALUES ("
for range values {
query += "?, "
}
query = query[:len(query)-2]
query += ")"
_, err := r.db.Exec(query, values...)
if err != nil {
return err
}
return nil
}
func (r *OrderRepository) DeleteOrder(id string) error {
_, err := r.db.Exec("DELETE FROM orders WHERE id = ?", id)
if err != nil {
return err
}
return nil
}
package order
import (
"context"
"errors"
)
var (
ErrGet = errors.New("could not get order")
ErrAdd = errors.New("could not add order")
ErrNotFound = errors.New("could not find order")
)
// Repo represents the layer to read/write data from/to the storage
// You can also consider splitting this interface into multiple ones
type Repo interface {
Get(context.Context, ID) (*Order, error)
Add(context.Context, *Order) error
}
package postgres
import (
"context"
"database/sql"
"errors"
"fmt"
_ "github.com/lib/pq"
)
type Repository struct {
db *sql.DB
}
func NewRepository(db *sql.DB) *Repository {
return &Repository{db}
}
func (r *Repository) Get(ctx context.Context, id order.ID) (*order.Order, error) {
query := "SELECT id, number, placedAt FROM orders WHERE id = $1"
row := r.db.QueryRowContext(ctx, query, id)
if err := row.Err(); err != nil {
return nil, fmt.Errorf("%w: could not perform query: %w", order.ErrGet, err)
}
o := &order.Order{}
if err := row.Scan(&o.ID, &o.Number, &o.PlacedAt); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("%w: %w", order.ErrNotFound, err)
}
return nil, fmt.Errorf("%w: could not scan resut: %w", order.ErrGet, err)
}
return o, nil
}
func (r *Repository) Add(ctx context.Context, o *order.Order) error {
query := "INSERT INTO orders (number, placedAt) VALUES ($1, $2)"
if _, err := r.db.ExecContext(ctx, query, o.Number, o.PlacedAt); err != nil {
return fmt.Errorf("%w: %w", order.ErrAdd, err)
}
return nil
}
package internal
import (
...
)
// Errors that the application layer exposes
var (
ErrNotPlaced = errors.New("order could not be placed")
ErrNotMarkedAsShipped = errors.New("order could not be marked as shipped")
ErrNotMarkedAsDelivered = errors.New("order could not be marked as delivered")
)
// Service represent the application layer
// it depends on the domain logic and can be used
// by any infrastructure layer as domain logic orchestrator
type Service struct {
repo order.Repo
}
// Place places an order and store it in the repository
func (s *Service) Place(
ctx context.Context,
n order.Number,
uID order.UserID,
) (*order.Order, error) {
o := order.Place(n, uID)
if err := s.repo.Add(ctx, o); err != nil {
return nil, fmt.Errorf("%w: %w", ErrNotPlaced, err)
}
return o, nil
}
// MarkAsShipped marks as shipped an order and store it in the repository
func (s *Service) MarkAsShipped(ctx context.Context, id order.ID) (*order.Order, error) {
o, err := s.repo.Get(ctx, id)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrNotMarkedAsShipped, err)
}
if err := o.MarkAsShipped(); err != nil {
return nil, fmt.Errorf("%w: %w", ErrNotMarkedAsShipped, err)
}
if err := s.repo.Add(ctx, o); err != nil {
return nil, fmt.Errorf("%w: %w", ErrNotMarkedAsShipped, err)
}
return o, nil
}
// MarkAsDelivered marks as delivered an order and store it in the repository
func (s *Service) MarkAsDelivered(ctx context.Context, id order.ID) (*order.Order, error) {
o, err := s.repo.Get(ctx, id)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrNotMarkedAsDelivered, err)
}
if err := o.MarkAsDelivered(); err != nil {
return nil, fmt.Errorf("%w: %w", ErrNotMarkedAsDelivered, err)
}
if err := s.repo.Add(ctx, o); err != nil {
return nil, fmt.Errorf("%w: %w", ErrNotMarkedAsDelivered, err)
}
return o, nil
}
Contacts