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

graph TB subgraph "E-commerce System" subgraph "Ordering Context" OC[Order Aggregate] OL[Order Line Items] OP[Order Payment] end subgraph "Inventory Context" IC[Product Catalog] IS[Stock Management] IW[Warehouse] end subgraph "Shipping Context" SC[Shipment] SD[Delivery] ST[Tracking] end subgraph "Customer Context" CC[Customer Profile] CA[Address] CP[Preferences] end end OC -.->|Anti-Corruption Layer| IC OC -.->|Shared Kernel| CC SC -.->|Published Language| OC style OC fill:#FFD700 style IC fill:#87CEEB style SC fill:#90EE90 style CC fill:#FFB6C1

Bounded Context Map

graph LR subgraph "Sales Context" S[Sales Domain
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

graph TD subgraph "Aggregate Root" AR[Order
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

  1. Ubiquitous Language: Use domain language in code and conversations
  2. Bounded Contexts: Define clear boundaries between contexts
  3. Aggregate Boundaries: Keep aggregates small and focused
  4. Immutable Value Objects: Make value objects immutable
  5. Domain Events: Use events to communicate between aggregates
  6. Repository per Aggregate: One repository per aggregate root
  7. Anemic Models: Avoid anemic domain models - put logic in entities

Common Pitfalls

  1. Large Aggregates: Creating aggregates that are too large
  2. Breaking Invariants: Modifying entities without going through aggregate root
  3. Transaction Boundaries: Spanning transactions across multiple aggregates
  4. Ignoring Ubiquitous Language: Not collaborating with domain experts
  5. Over-engineering: Applying DDD to simple CRUD applications
  6. 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 →