Go Architecture Patterns Series: ← Hexagonal Architecture | Series Overview | Next: Modular Monolith →
What is Domain-Driven Design?
Domain-Driven Design (DDD) is a software development approach introduced by Eric Evans that focuses on creating software that matches the business domain. It emphasizes collaboration between technical and domain experts using a common language (Ubiquitous Language) and strategic/tactical patterns to handle complex business logic.
Key Principles:
- Ubiquitous Language: Common language shared by developers and domain experts
- Bounded Contexts: Explicit boundaries where a particular domain model applies
- Domain Model: Rich model that captures business rules and behavior
- Strategic Design: High-level patterns for organizing large systems
- Tactical Design: Building blocks for implementing domain models
DDD Strategic Patterns
Bounded Context Map
Customer, Order, Product] end subgraph "Support Context" SP[Support Domain
Ticket, Customer, Issue] end subgraph "Billing Context" B[Billing Domain
Invoice, Payment, Customer] end subgraph "Shared Kernel" SK[Customer Identity] end S -.->|Conformist| SK SP -.->|Customer/Supplier| S B -.->|Partnership| S style S fill:#FFD700 style SP fill:#87CEEB style B fill:#90EE90 style SK fill:#FFB6C1
DDD Tactical Patterns
Aggregate Root] E1[Order Line
Entity] E2[Payment Info
Entity] VO1[Money
Value Object] VO2[Address
Value Object] end subgraph "Domain Services" DS[Pricing Service] DS2[Shipping Calculator] end subgraph "Repositories" R[Order Repository] end AR --> E1 AR --> E2 E1 --> VO1 AR --> VO2 AR -.->|uses| DS R -.->|persists| AR style AR fill:#FFD700,stroke:#FF8C00,stroke-width:3px style VO1 fill:#87CEEB style DS fill:#90EE90 style R fill:#FFB6C1
Real-World Use Cases
- E-commerce Platforms: Complex ordering, inventory, and payment systems
- Financial Systems: Banking, trading, and payment processing
- Healthcare Systems: Patient records, appointments, and billing
- Supply Chain Management: Inventory, shipping, and logistics
- Enterprise Resource Planning: Multi-domain business systems
- Insurance Systems: Policy management, claims, and underwriting
Project Structure
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── domain/
│ │ ├── order/ # Order Bounded Context
│ │ │ ├── aggregate/
│ │ │ │ └── order.go # Order aggregate root
│ │ │ ├── entity/
│ │ │ │ └── order_line.go
│ │ │ ├── valueobject/
│ │ │ │ ├── money.go
│ │ │ │ └── quantity.go
│ │ │ ├── service/
│ │ │ │ └── pricing_service.go
│ │ │ ├── repository/
│ │ │ │ └── order_repository.go
│ │ │ └── event/
│ │ │ └── order_placed.go
│ │ ├── customer/ # Customer Bounded Context
│ │ │ ├── aggregate/
│ │ │ └── valueobject/
│ │ └── shared/ # Shared Kernel
│ │ └── valueobject/
│ ├── application/ # Application Services
│ │ ├── order/
│ │ │ └── order_service.go
│ │ └── customer/
│ │ └── customer_service.go
│ └── infrastructure/
│ ├── persistence/
│ └── messaging/
└── go.mod
Building Blocks: Value Objects
package valueobject
import (
"errors"
"fmt"
)
// Money represents a monetary value (Value Object)
// Value objects are immutable and compared by value, not identity
type Money struct {
amount int64 // Amount in smallest currency unit (cents)
currency string
}
// NewMoney creates a new Money value object
func NewMoney(amount int64, currency string) (Money, error) {
if currency == "" {
return Money{}, errors.New("currency cannot be empty")
}
if amount < 0 {
return Money{}, errors.New("amount cannot be negative")
}
return Money{amount: amount, currency: currency}, nil
}
// Amount returns the amount
func (m Money) Amount() int64 {
return m.amount
}
// Currency returns the currency
func (m Money) Currency() string {
return m.currency
}
// Add adds two money values
func (m Money) Add(other Money) (Money, error) {
if m.currency != other.currency {
return Money{}, errors.New("cannot add different currencies")
}
return Money{
amount: m.amount + other.amount,
currency: m.currency,
}, nil
}
// Multiply multiplies money by a quantity
func (m Money) Multiply(multiplier int) Money {
return Money{
amount: m.amount * int64(multiplier),
currency: m.currency,
}
}
// IsZero checks if money is zero
func (m Money) IsZero() bool {
return m.amount == 0
}
// Equals checks equality (value objects compare by value)
func (m Money) Equals(other Money) bool {
return m.amount == other.amount && m.currency == other.currency
}
// String returns string representation
func (m Money) String() string {
return fmt.Sprintf("%d %s", m.amount, m.currency)
}
// Email represents an email address (Value Object)
type Email struct {
value string
}
// NewEmail creates a new Email value object
func NewEmail(email string) (Email, error) {
if !isValidEmail(email) {
return Email{}, errors.New("invalid email format")
}
return Email{value: email}, nil
}
// String returns the email string
func (e Email) String() string {
return e.value
}
// Equals checks equality
func (e Email) Equals(other Email) bool {
return e.value == other.value
}
func isValidEmail(email string) bool {
// Simplified validation
return len(email) > 3 && contains(email, "@") && contains(email, ".")
}
func contains(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
// Address represents a physical address (Value Object)
type Address struct {
street string
city string
state string
postalCode string
country string
}
// NewAddress creates a new Address value object
func NewAddress(street, city, state, postalCode, country string) (Address, error) {
if street == "" || city == "" || country == "" {
return Address{}, errors.New("street, city, and country are required")
}
return Address{
street: street,
city: city,
state: state,
postalCode: postalCode,
country: country,
}, nil
}
// Street returns the street
func (a Address) Street() string { return a.street }
// City returns the city
func (a Address) City() string { return a.city }
// Country returns the country
func (a Address) Country() string { return a.country }
// Quantity represents a product quantity (Value Object)
type Quantity struct {
value int
}
// NewQuantity creates a new Quantity value object
func NewQuantity(value int) (Quantity, error) {
if value < 0 {
return Quantity{}, errors.New("quantity cannot be negative")
}
return Quantity{value: value}, nil
}
// Value returns the quantity value
func (q Quantity) Value() int {
return q.value
}
// Add adds two quantities
func (q Quantity) Add(other Quantity) Quantity {
return Quantity{value: q.value + other.value}
}
// IsZero checks if quantity is zero
func (q Quantity) IsZero() bool {
return q.value == 0
}
Building Blocks: Entities
package entity
import (
"time"
"myapp/internal/domain/order/valueobject"
)
// OrderLine is an entity (has identity)
// Entities have identity and lifecycle
type OrderLine struct {
id string
productID string
product string
quantity valueobject.Quantity
unitPrice valueobject.Money
createdAt time.Time
}
// NewOrderLine creates a new order line entity
func NewOrderLine(id, productID, product string, quantity valueobject.Quantity, unitPrice valueobject.Money) *OrderLine {
return &OrderLine{
id: id,
productID: productID,
product: product,
quantity: quantity,
unitPrice: unitPrice,
createdAt: time.Now(),
}
}
// ID returns the order line ID (identity)
func (ol *OrderLine) ID() string {
return ol.id
}
// ProductID returns the product ID
func (ol *OrderLine) ProductID() string {
return ol.productID
}
// Quantity returns the quantity
func (ol *OrderLine) Quantity() valueobject.Quantity {
return ol.quantity
}
// UnitPrice returns the unit price
func (ol *OrderLine) UnitPrice() valueobject.Money {
return ol.unitPrice
}
// TotalPrice calculates the total price for this line
func (ol *OrderLine) TotalPrice() valueobject.Money {
return ol.unitPrice.Multiply(ol.quantity.Value())
}
// UpdateQuantity updates the quantity
func (ol *OrderLine) UpdateQuantity(newQuantity valueobject.Quantity) {
ol.quantity = newQuantity
}
// Payment is an entity representing payment information
type Payment struct {
id string
method PaymentMethod
amount valueobject.Money
transactionID string
status PaymentStatus
paidAt time.Time
}
// PaymentMethod represents payment method
type PaymentMethod string
const (
PaymentMethodCreditCard PaymentMethod = "credit_card"
PaymentMethodDebitCard PaymentMethod = "debit_card"
PaymentMethodPayPal PaymentMethod = "paypal"
)
// PaymentStatus represents payment status
type PaymentStatus string
const (
PaymentStatusPending PaymentStatus = "pending"
PaymentStatusCompleted PaymentStatus = "completed"
PaymentStatusFailed PaymentStatus = "failed"
)
Building Blocks: Aggregates
package aggregate
import (
"errors"
"time"
"myapp/internal/domain/order/entity"
"myapp/internal/domain/order/event"
"myapp/internal/domain/order/valueobject"
)
// Order is an aggregate root
// Aggregate roots maintain consistency boundaries and control access to entities
type Order struct {
id string
customerID string
orderLines []*entity.OrderLine
shippingAddress valueobject.Address
billingAddress valueobject.Address
payment *entity.Payment
status OrderStatus
total valueobject.Money
createdAt time.Time
updatedAt time.Time
// Domain events
events []event.DomainEvent
}
// OrderStatus represents the status of an order
type OrderStatus string
const (
OrderStatusDraft OrderStatus = "draft"
OrderStatusPlaced OrderStatus = "placed"
OrderStatusConfirmed OrderStatus = "confirmed"
OrderStatusShipped OrderStatus = "shipped"
OrderStatusDelivered OrderStatus = "delivered"
OrderStatusCancelled OrderStatus = "cancelled"
)
// NewOrder creates a new order aggregate
func NewOrder(id, customerID string, shippingAddress, billingAddress valueobject.Address) (*Order, error) {
if id == "" {
return nil, errors.New("order ID is required")
}
if customerID == "" {
return nil, errors.New("customer ID is required")
}
return &Order{
id: id,
customerID: customerID,
orderLines: make([]*entity.OrderLine, 0),
shippingAddress: shippingAddress,
billingAddress: billingAddress,
status: OrderStatusDraft,
createdAt: time.Now(),
updatedAt: time.Now(),
events: make([]event.DomainEvent, 0),
}, nil
}
// ID returns the order ID
func (o *Order) ID() string {
return o.id
}
// CustomerID returns the customer ID
func (o *Order) CustomerID() string {
return o.customerID
}
// Status returns the order status
func (o *Order) Status() OrderStatus {
return o.status
}
// Total returns the total amount
func (o *Order) Total() valueobject.Money {
return o.total
}
// AddOrderLine adds an order line to the order (aggregate invariant)
func (o *Order) AddOrderLine(orderLine *entity.OrderLine) error {
// Business rule: Cannot add lines to non-draft orders
if o.status != OrderStatusDraft {
return errors.New("cannot add items to non-draft order")
}
// Business rule: Check for duplicate products
for _, line := range o.orderLines {
if line.ProductID() == orderLine.ProductID() {
return errors.New("product already exists in order")
}
}
o.orderLines = append(o.orderLines, orderLine)
o.recalculateTotal()
o.updatedAt = time.Now()
return nil
}
// RemoveOrderLine removes an order line (aggregate invariant)
func (o *Order) RemoveOrderLine(orderLineID string) error {
// Business rule: Cannot remove lines from non-draft orders
if o.status != OrderStatusDraft {
return errors.New("cannot remove items from non-draft order")
}
for i, line := range o.orderLines {
if line.ID() == orderLineID {
o.orderLines = append(o.orderLines[:i], o.orderLines[i+1:]...)
o.recalculateTotal()
o.updatedAt = time.Now()
return nil
}
}
return errors.New("order line not found")
}
// PlaceOrder places the order (state transition)
func (o *Order) PlaceOrder() error {
// Business rule: Can only place draft orders
if o.status != OrderStatusDraft {
return errors.New("can only place draft orders")
}
// Business rule: Order must have at least one line
if len(o.orderLines) == 0 {
return errors.New("order must have at least one item")
}
// Business rule: Order must have payment
if o.payment == nil {
return errors.New("order must have payment information")
}
o.status = OrderStatusPlaced
o.updatedAt = time.Now()
// Raise domain event
o.addEvent(event.NewOrderPlacedEvent(o.id, o.customerID, o.total))
return nil
}
// ConfirmOrder confirms the order
func (o *Order) ConfirmOrder() error {
if o.status != OrderStatusPlaced {
return errors.New("can only confirm placed orders")
}
o.status = OrderStatusConfirmed
o.updatedAt = time.Now()
o.addEvent(event.NewOrderConfirmedEvent(o.id))
return nil
}
// ShipOrder marks the order as shipped
func (o *Order) ShipOrder() error {
if o.status != OrderStatusConfirmed {
return errors.New("can only ship confirmed orders")
}
o.status = OrderStatusShipped
o.updatedAt = time.Now()
o.addEvent(event.NewOrderShippedEvent(o.id, o.shippingAddress))
return nil
}
// CancelOrder cancels the order
func (o *Order) CancelOrder(reason string) error {
// Business rule: Cannot cancel shipped or delivered orders
if o.status == OrderStatusShipped || o.status == OrderStatusDelivered {
return errors.New("cannot cancel shipped or delivered orders")
}
if o.status == OrderStatusCancelled {
return errors.New("order is already cancelled")
}
o.status = OrderStatusCancelled
o.updatedAt = time.Now()
o.addEvent(event.NewOrderCancelledEvent(o.id, reason))
return nil
}
// AddPayment adds payment to the order
func (o *Order) AddPayment(payment *entity.Payment) error {
if o.payment != nil {
return errors.New("payment already exists")
}
// Business rule: Payment amount must match order total
if !payment.Amount.Equals(o.total) {
return errors.New("payment amount must match order total")
}
o.payment = payment
o.updatedAt = time.Now()
return nil
}
// recalculateTotal recalculates the order total
func (o *Order) recalculateTotal() {
if len(o.orderLines) == 0 {
o.total = valueobject.Money{}
return
}
total := o.orderLines[0].TotalPrice()
for i := 1; i < len(o.orderLines); i++ {
var err error
total, err = total.Add(o.orderLines[i].TotalPrice())
if err != nil {
// Handle error - in production, log this
return
}
}
o.total = total
}
// GetDomainEvents returns all domain events
func (o *Order) GetDomainEvents() []event.DomainEvent {
return o.events
}
// ClearDomainEvents clears all domain events
func (o *Order) ClearDomainEvents() {
o.events = make([]event.DomainEvent, 0)
}
// addEvent adds a domain event
func (o *Order) addEvent(e event.DomainEvent) {
o.events = append(o.events, e)
}
// OrderLines returns a copy of order lines
func (o *Order) OrderLines() []*entity.OrderLine {
// Return copy to prevent external modification
lines := make([]*entity.OrderLine, len(o.orderLines))
copy(lines, o.orderLines)
return lines
}
Building Blocks: Domain Events
package event
import (
"time"
"myapp/internal/domain/order/valueobject"
)
// DomainEvent is the base interface for all domain events
type DomainEvent interface {
OccurredAt() time.Time
EventType() string
}
// OrderPlacedEvent is raised when an order is placed
type OrderPlacedEvent struct {
orderID string
customerID string
total valueobject.Money
occurredAt time.Time
}
// NewOrderPlacedEvent creates a new OrderPlacedEvent
func NewOrderPlacedEvent(orderID, customerID string, total valueobject.Money) *OrderPlacedEvent {
return &OrderPlacedEvent{
orderID: orderID,
customerID: customerID,
total: total,
occurredAt: time.Now(),
}
}
// OrderID returns the order ID
func (e *OrderPlacedEvent) OrderID() string {
return e.orderID
}
// CustomerID returns the customer ID
func (e *OrderPlacedEvent) CustomerID() string {
return e.customerID
}
// Total returns the total amount
func (e *OrderPlacedEvent) Total() valueobject.Money {
return e.total
}
// OccurredAt returns when the event occurred
func (e *OrderPlacedEvent) OccurredAt() time.Time {
return e.occurredAt
}
// EventType returns the event type
func (e *OrderPlacedEvent) EventType() string {
return "OrderPlaced"
}
// OrderConfirmedEvent is raised when an order is confirmed
type OrderConfirmedEvent struct {
orderID string
occurredAt time.Time
}
// NewOrderConfirmedEvent creates a new OrderConfirmedEvent
func NewOrderConfirmedEvent(orderID string) *OrderConfirmedEvent {
return &OrderConfirmedEvent{
orderID: orderID,
occurredAt: time.Now(),
}
}
// OrderID returns the order ID
func (e *OrderConfirmedEvent) OrderID() string {
return e.orderID
}
// OccurredAt returns when the event occurred
func (e *OrderConfirmedEvent) OccurredAt() time.Time {
return e.occurredAt
}
// EventType returns the event type
func (e *OrderConfirmedEvent) EventType() string {
return "OrderConfirmed"
}
// OrderShippedEvent is raised when an order is shipped
type OrderShippedEvent struct {
orderID string
shippingAddress valueobject.Address
occurredAt time.Time
}
// NewOrderShippedEvent creates a new OrderShippedEvent
func NewOrderShippedEvent(orderID string, shippingAddress valueobject.Address) *OrderShippedEvent {
return &OrderShippedEvent{
orderID: orderID,
shippingAddress: shippingAddress,
occurredAt: time.Now(),
}
}
// EventType returns the event type
func (e *OrderShippedEvent) EventType() string {
return "OrderShipped"
}
// OccurredAt returns when the event occurred
func (e *OrderShippedEvent) OccurredAt() time.Time {
return e.occurredAt
}
// OrderCancelledEvent is raised when an order is cancelled
type OrderCancelledEvent struct {
orderID string
reason string
occurredAt time.Time
}
// NewOrderCancelledEvent creates a new OrderCancelledEvent
func NewOrderCancelledEvent(orderID, reason string) *OrderCancelledEvent {
return &OrderCancelledEvent{
orderID: orderID,
reason: reason,
occurredAt: time.Now(),
}
}
// EventType returns the event type
func (e *OrderCancelledEvent) EventType() string {
return "OrderCancelled"
}
// OccurredAt returns when the event occurred
func (e *OrderCancelledEvent) OccurredAt() time.Time {
return e.occurredAt
}
Building Blocks: Domain Services
package service
import (
"errors"
"myapp/internal/domain/order/valueobject"
)
// PricingService is a domain service for calculating prices
// Domain services contain business logic that doesn't belong to any entity
type PricingService struct {
taxRate float64
}
// NewPricingService creates a new pricing service
func NewPricingService(taxRate float64) *PricingService {
return &PricingService{taxRate: taxRate}
}
// CalculateOrderTotal calculates the total price with tax and shipping
func (s *PricingService) CalculateOrderTotal(
subtotal valueobject.Money,
shippingCost valueobject.Money,
) (valueobject.Money, error) {
// Add shipping to subtotal
total, err := subtotal.Add(shippingCost)
if err != nil {
return valueobject.Money{}, err
}
// Calculate tax
taxAmount := int64(float64(total.Amount()) * s.taxRate)
tax, err := valueobject.NewMoney(taxAmount, total.Currency())
if err != nil {
return valueobject.Money{}, err
}
// Add tax to total
return total.Add(tax)
}
// ApplyDiscount applies discount to money
func (s *PricingService) ApplyDiscount(amount valueobject.Money, discountPercent int) (valueobject.Money, error) {
if discountPercent < 0 || discountPercent > 100 {
return valueobject.Money{}, errors.New("invalid discount percentage")
}
discountAmount := amount.Amount() * int64(discountPercent) / 100
finalAmount := amount.Amount() - discountAmount
return valueobject.NewMoney(finalAmount, amount.Currency())
}
// ShippingCalculator is a domain service for calculating shipping costs
type ShippingCalculator struct{}
// NewShippingCalculator creates a new shipping calculator
func NewShippingCalculator() *ShippingCalculator {
return &ShippingCalculator{}
}
// CalculateShippingCost calculates shipping cost based on weight and distance
func (s *ShippingCalculator) CalculateShippingCost(
weightKg float64,
distanceKm float64,
currency string,
) (valueobject.Money, error) {
// Simple formula: base rate + weight factor + distance factor
baseRate := int64(500) // $5.00 base
weightFactor := int64(weightKg * 100)
distanceFactor := int64(distanceKm * 10)
totalCost := baseRate + weightFactor + distanceFactor
return valueobject.NewMoney(totalCost, currency)
}
Building Blocks: Repositories
package repository
import (
"context"
"myapp/internal/domain/order/aggregate"
)
// OrderRepository defines the repository interface for orders
// Repositories provide collection-like interface for aggregates
type OrderRepository interface {
// Save saves an order aggregate
Save(ctx context.Context, order *aggregate.Order) error
// FindByID finds an order by ID
FindByID(ctx context.Context, id string) (*aggregate.Order, error)
// FindByCustomerID finds orders by customer ID
FindByCustomerID(ctx context.Context, customerID string) ([]*aggregate.Order, error)
// Update updates an existing order
Update(ctx context.Context, order *aggregate.Order) error
// Delete deletes an order
Delete(ctx context.Context, id string) error
// NextIdentity generates the next order identity
NextIdentity() string
}
Application Service
package application
import (
"context"
"fmt"
"myapp/internal/domain/order/aggregate"
"myapp/internal/domain/order/entity"
"myapp/internal/domain/order/repository"
"myapp/internal/domain/order/service"
"myapp/internal/domain/order/valueobject"
)
// OrderService is an application service that orchestrates use cases
// Application services coordinate domain objects and infrastructure
type OrderService struct {
orderRepo repository.OrderRepository
pricingService *service.PricingService
shippingCalculator *service.ShippingCalculator
eventPublisher EventPublisher
}
// EventPublisher publishes domain events
type EventPublisher interface {
Publish(ctx context.Context, events []event.DomainEvent) error
}
// NewOrderService creates a new order service
func NewOrderService(
orderRepo repository.OrderRepository,
pricingService *service.PricingService,
shippingCalculator *service.ShippingCalculator,
eventPublisher EventPublisher,
) *OrderService {
return &OrderService{
orderRepo: orderRepo,
pricingService: pricingService,
shippingCalculator: shippingCalculator,
eventPublisher: eventPublisher,
}
}
// CreateOrderCommand represents the command to create an order
type CreateOrderCommand struct {
CustomerID string
ShippingAddress AddressDTO
BillingAddress AddressDTO
Items []OrderItemDTO
}
// AddressDTO is a data transfer object for address
type AddressDTO struct {
Street string
City string
State string
PostalCode string
Country string
}
// OrderItemDTO is a data transfer object for order items
type OrderItemDTO struct {
ProductID string
Product string
Quantity int
UnitPrice MoneyDTO
}
// MoneyDTO is a data transfer object for money
type MoneyDTO struct {
Amount int64
Currency string
}
// CreateOrder creates a new order (use case)
func (s *OrderService) CreateOrder(ctx context.Context, cmd CreateOrderCommand) (string, error) {
// Convert DTOs to value objects
shippingAddr, err := valueobject.NewAddress(
cmd.ShippingAddress.Street,
cmd.ShippingAddress.City,
cmd.ShippingAddress.State,
cmd.ShippingAddress.PostalCode,
cmd.ShippingAddress.Country,
)
if err != nil {
return "", fmt.Errorf("invalid shipping address: %w", err)
}
billingAddr, err := valueobject.NewAddress(
cmd.BillingAddress.Street,
cmd.BillingAddress.City,
cmd.BillingAddress.State,
cmd.BillingAddress.PostalCode,
cmd.BillingAddress.Country,
)
if err != nil {
return "", fmt.Errorf("invalid billing address: %w", err)
}
// Create order aggregate
orderID := s.orderRepo.NextIdentity()
order, err := aggregate.NewOrder(orderID, cmd.CustomerID, shippingAddr, billingAddr)
if err != nil {
return "", err
}
// Add order lines
for _, item := range cmd.Items {
quantity, err := valueobject.NewQuantity(item.Quantity)
if err != nil {
return "", err
}
unitPrice, err := valueobject.NewMoney(item.UnitPrice.Amount, item.UnitPrice.Currency)
if err != nil {
return "", err
}
orderLine := entity.NewOrderLine(
fmt.Sprintf("%s-line-%s", orderID, item.ProductID),
item.ProductID,
item.Product,
quantity,
unitPrice,
)
if err := order.AddOrderLine(orderLine); err != nil {
return "", err
}
}
// Save order
if err := s.orderRepo.Save(ctx, order); err != nil {
return "", fmt.Errorf("failed to save order: %w", err)
}
return orderID, nil
}
// PlaceOrder places an order (use case)
func (s *OrderService) PlaceOrder(ctx context.Context, orderID string, payment PaymentDTO) error {
// Load order aggregate
order, err := s.orderRepo.FindByID(ctx, orderID)
if err != nil {
return fmt.Errorf("order not found: %w", err)
}
// Create payment entity
paymentAmount, err := valueobject.NewMoney(payment.Amount, payment.Currency)
if err != nil {
return err
}
paymentEntity := &entity.Payment{
// Payment entity fields
}
// Add payment to order
if err := order.AddPayment(paymentEntity); err != nil {
return err
}
// Place order (this enforces business rules)
if err := order.PlaceOrder(); err != nil {
return err
}
// Save order
if err := s.orderRepo.Update(ctx, order); err != nil {
return fmt.Errorf("failed to update order: %w", err)
}
// Publish domain events
events := order.GetDomainEvents()
if err := s.eventPublisher.Publish(ctx, events); err != nil {
// Log error but don't fail the transaction
fmt.Printf("failed to publish events: %v\n", err)
}
order.ClearDomainEvents()
return nil
}
// PaymentDTO is a data transfer object for payment
type PaymentDTO struct {
Amount int64
Currency string
Method string
}
Best Practices
- Ubiquitous Language: Use domain language in code and conversations
- Bounded Contexts: Define clear boundaries between contexts
- Aggregate Boundaries: Keep aggregates small and focused
- Immutable Value Objects: Make value objects immutable
- Domain Events: Use events to communicate between aggregates
- Repository per Aggregate: One repository per aggregate root
- Anemic Models: Avoid anemic domain models - put logic in entities
Common Pitfalls
- Large Aggregates: Creating aggregates that are too large
- Breaking Invariants: Modifying entities without going through aggregate root
- Transaction Boundaries: Spanning transactions across multiple aggregates
- Ignoring Ubiquitous Language: Not collaborating with domain experts
- Over-engineering: Applying DDD to simple CRUD applications
- Missing Bounded Contexts: Not identifying context boundaries
When to Use DDD
Use When:
- Complex business domains with rich business logic
- Long-term projects that will evolve
- Close collaboration with domain experts is possible
- Team has experience with DDD patterns
- Business logic is the core value of the system
Avoid When:
- Simple CRUD applications
- Data-centric systems with little business logic
- Rapid prototyping phase
- Team lacks DDD experience and training time
- Business domain is well-understood and stable
Advantages
- Domain Focus: Business logic is central and explicit
- Ubiquitous Language: Clear communication between technical and business teams
- Flexibility: Easy to evolve as business requirements change
- Testability: Business logic can be tested in isolation
- Maintainability: Clear structure and boundaries
Disadvantages
- Complexity: Requires significant up-front design
- Learning Curve: Team needs DDD knowledge
- Overhead: More code and abstractions than simple approaches
- Requires Experts: Need domain expert involvement
- Over-engineering Risk: Can be excessive for simple domains
Domain-Driven Design provides powerful patterns for tackling complex business domains by focusing on the domain model, using ubiquitous language, and applying strategic and tactical design patterns.
Go Architecture Patterns Series: ← Hexagonal Architecture | Series Overview | Next: Modular Monolith →