Domain-Driven Design (DDD) is an approach to software development that focuses on understanding and modeling the business domain. In this post, we'll explore how to implement DDD principles using Go, with practical examples and clear explanations.
What is Domain-Driven Design?
DDD is a methodology that emphasizes:
- Close collaboration between technical and domain experts
- Creating a shared understanding of the business domain
- Building software that reflects the business model
Key Building Blocks of DDD in Go
1. Value Objects
Value Objects are immutable objects that have no identity. They are defined by their attributes.
type Money struct {
amount decimal.Decimal
currency string
}
func NewMoney(amount decimal.Decimal, currency string) Money {
return Money{
amount: amount,
currency: currency,
}
}
func (m Money) Equals(other Money) bool {
return m.amount.Equals(other.amount) && m.currency == other.currency
}
2. Entities
Entities are objects with a unique identity that persists throughout their lifecycle.
type Order struct {
ID string
CustomerID string
Items []OrderItem
TotalAmount Money
Status OrderStatus
}
func NewOrder(id string, customerID string) *Order {
return &Order{
ID: id,
CustomerID: customerID,
Items: make([]OrderItem, 0),
Status: OrderStatusPending,
}
}
3. Aggregates
Aggregates are clusters of related entities and value objects treated as a single unit.
type Order struct {
func (o *Order) AddItem(item OrderItem) error {
if o.Status != OrderStatusPending {
return errors.New("cannot add items to non-pending order")
}
o.Items = append(o.Items, item)
o.recalculateTotal()
return nil
}
func (o *Order) recalculateTotal() {
}
}
4. Repositories
Repositories handle persistence of aggregates.
type OrderRepository interface {
Save(ctx context.Context, order *Order) error
FindByID(ctx context.Context, id string) (*Order, error)
FindByCustomer(ctx context.Context, customerID string) ([]*Order, error)
}
type PostgresOrderRepository struct {
db *sql.DB
}
func (r *PostgresOrderRepository) Save(ctx context.Context, order *Order) error {
}
5. Domain Services
Domain Services handle operations that don't naturally fit within entities or value objects.
type OrderProcessor interface {
ProcessOrder(ctx context.Context, order *Order) error
}
type OrderProcessorService struct {
orderRepo OrderRepository
paymentService PaymentService
}
func (s *OrderProcessorService) ProcessOrder(ctx context.Context, order *Order) error {
}
Organizing Your Go Project with DDD
Here's a typical project structure following DDD principles:
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── domain/
│ │ ├── order.go
│ │ ├── customer.go
│ │ └── money.go
│ ├── repository/
│ │ └── order.go
│ ├── service/
│ │ └── order.go
│ └── application/
│ └── orderservice.go
└── pkg/
└── shared/
Best Practices
- Use Interfaces: Define clear boundaries between layers using interfaces.
type OrderService interface {
CreateOrder(ctx context.Context, customerID string) (*Order, error)
AddItem(ctx context.Context, orderID string, item OrderItem) error
}
- Implement Validation: Use value objects to ensure data validity.
func NewEmail(address string) (Email, error) {
if !isValidEmail(address) {
return Email{}, errors.New("invalid email address")
}
return Email{address: address}, nil
}
- Handle Errors: Create domain-specific errors.
type DomainError struct {
Message string
Code string
}
func (e *DomainError) Error() string {
return e.Message
}
var ErrInvalidOrderStatus = &DomainError{
Message: "invalid order status",
Code: "INVALID_ORDER_STATUS",
}
Conclusion
DDD in Go helps create maintainable and scalable applications by:
- Separating concerns through clear boundaries
- Enforcing business rules at the domain level
- Creating a shared understanding between technical and domain experts
Remember that DDD is not about the code structure alone - it's about understanding and modeling the business domain effectively. The Go programming language, with its simplicity and strong typing, provides an excellent foundation for implementing DDD principles.
This is just an introduction to DDD in Go. As you dive deeper, you'll discover more patterns and practices that can help you build better domain-driven applications.