Domain-Driven Design (DDD)

Domain-Driven Design (DDD) adalah pendekatan untuk mengembangkan software yang kompleks dengan menghubungkan implementasi dengan model bisnis yang berkembang.

Contoh Masalah

Bagaimana cara:

  1. Implement DDD patterns
  2. Structure domain model
  3. Handle business rules
  4. Manage bounded contexts

Penyelesaian

1. Struktur Project

ecommerce/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── domain/
│   │   ├── order/
│   │   │   ├── entity.go
│   │   │   ├── repository.go
│   │   │   ├── service.go
│   │   │   └── value_objects.go
│   │   └── product/
│   │       ├── entity.go
│   │       ├── repository.go
│   │       └── service.go
│   ├── infrastructure/
│   │   ├── persistence/
│   │   │   └── postgres/
│   │   │       └── repository.go
│   │   └── http/
│   │       └── rest/
│   │           └── handler.go
│   └── application/
│       └── service.go
└── pkg/
    └── common/
        ├── errors.go
        └── valueobjects.go

2. Domain Layer

internal/domain/order/entity.go:

package order

import (
    "time"
    
    "ecommerce/pkg/common"
)

// Order adalah aggregate root
type Order struct {
    ID            string
    CustomerID    string
    Items         []OrderItem
    Status        OrderStatus
    TotalAmount   Money
    CreatedAt     time.Time
    UpdatedAt     time.Time
}

// NewOrder adalah factory method
func NewOrder(customerID string, items []OrderItem) (*Order, error) {
    if customerID == "" {
        return nil, common.ErrInvalidInput
    }
    
    if len(items) == 0 {
        return nil, common.ErrInvalidInput
    }

    total := calculateTotal(items)
    
    return &Order{
        ID:          generateID(),
        CustomerID:  customerID,
        Items:       items,
        Status:      OrderStatusPending,
        TotalAmount: total,
        CreatedAt:   time.Now(),
        UpdatedAt:   time.Now(),
    }, nil
}

// AddItem adalah domain method
func (o *Order) AddItem(item OrderItem) error {
    if o.Status != OrderStatusPending {
        return ErrOrderNotModifiable
    }

    o.Items = append(o.Items, item)
    o.TotalAmount = calculateTotal(o.Items)
    o.UpdatedAt = time.Now()
    
    return nil
}

// ConfirmOrder adalah domain method
func (o *Order) ConfirmOrder() error {
    if o.Status != OrderStatusPending {
        return ErrInvalidOrderStatus
    }

    o.Status = OrderStatusConfirmed
    o.UpdatedAt = time.Now()
    
    return nil
}

func calculateTotal(items []OrderItem) Money {
    var total float64
    for _, item := range items {
        total += item.Price.Amount * float64(item.Quantity)
    }
    return NewMoney(total, "USD")
}

internal/domain/order/value_objects.go:

package order

// OrderStatus adalah value object
type OrderStatus string

const (
    OrderStatusPending   OrderStatus = "pending"
    OrderStatusConfirmed OrderStatus = "confirmed"
    OrderStatusShipped   OrderStatus = "shipped"
    OrderStatusDelivered OrderStatus = "delivered"
    OrderStatusCancelled OrderStatus = "cancelled"
)

// Money adalah value object
type Money struct {
    Amount   float64
    Currency string
}

func NewMoney(amount float64, currency string) Money {
    return Money{
        Amount:   amount,
        Currency: currency,
    }
}

// OrderItem adalah value object
type OrderItem struct {
    ProductID string
    Quantity  int
    Price     Money
}

func NewOrderItem(productID string, quantity int,
    price Money) OrderItem {
    return OrderItem{
        ProductID: productID,
        Quantity:  quantity,
        Price:     price,
    }
}

internal/domain/order/repository.go:

package order

// OrderRepository adalah repository interface
type OrderRepository interface {
    Save(order *Order) error
    FindByID(id string) (*Order, error)
    FindByCustomerID(customerID string) ([]*Order, error)
    Update(order *Order) error
}

internal/domain/order/service.go:

package order

// OrderService adalah domain service
type OrderService struct {
    repo OrderRepository
}

func NewOrderService(repo OrderRepository) *OrderService {
    return &OrderService{
        repo: repo,
    }
}

func (s *OrderService) CreateOrder(customerID string,
    items []OrderItem) (*Order, error) {
    order, err := NewOrder(customerID, items)
    if err != nil {
        return nil, err
    }

    if err := s.repo.Save(order); err != nil {
        return nil, err
    }

    return order, nil
}

func (s *OrderService) ConfirmOrder(orderID string) error {
    order, err := s.repo.FindByID(orderID)
    if err != nil {
        return err
    }

    if err := order.ConfirmOrder(); err != nil {
        return err
    }

    return s.repo.Update(order)
}

3. Infrastructure Layer

internal/infrastructure/persistence/postgres/repository.go:

package postgres

import (
    "database/sql"
    "encoding/json"
    
    "ecommerce/internal/domain/order"
)

type OrderRepository struct {
    db *sql.DB
}

func NewOrderRepository(db *sql.DB) *OrderRepository {
    return &OrderRepository{
        db: db,
    }
}

func (r *OrderRepository) Save(order *order.Order) error {
    items, err := json.Marshal(order.Items)
    if err != nil {
        return err
    }

    query := `
        INSERT INTO orders (
            id, customer_id, items, status,
            total_amount, currency, created_at, updated_at
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
    `

    _, err = r.db.Exec(query,
        order.ID,
        order.CustomerID,
        items,
        order.Status,
        order.TotalAmount.Amount,
        order.TotalAmount.Currency,
        order.CreatedAt,
        order.UpdatedAt,
    )

    return err
}

func (r *OrderRepository) FindByID(id string) (*order.Order, error) {
    query := `
        SELECT id, customer_id, items, status,
               total_amount, currency, created_at, updated_at
        FROM orders
        WHERE id = $1
    `

    var o order.Order
    var items []byte
    var status string

    err := r.db.QueryRow(query, id).Scan(
        &o.ID,
        &o.CustomerID,
        &items,
        &status,
        &o.TotalAmount.Amount,
        &o.TotalAmount.Currency,
        &o.CreatedAt,
        &o.UpdatedAt,
    )
    if err != nil {
        return nil, err
    }

    if err := json.Unmarshal(items, &o.Items); err != nil {
        return nil, err
    }

    o.Status = order.OrderStatus(status)

    return &o, nil
}

4. Application Layer

internal/application/service.go:

package application

import (
    "ecommerce/internal/domain/order"
)

type OrderApplicationService struct {
    orderService *order.OrderService
}

func NewOrderApplicationService(
    orderService *order.OrderService) *OrderApplicationService {
    return &OrderApplicationService{
        orderService: orderService,
    }
}

type CreateOrderRequest struct {
    CustomerID string       `json:"customerId"`
    Items      []OrderItem `json:"items"`
}

type OrderItem struct {
    ProductID string  `json:"productId"`
    Quantity  int     `json:"quantity"`
    Price     float64 `json:"price"`
}

type CreateOrderResponse struct {
    OrderID     string  `json:"orderId"`
    TotalAmount float64 `json:"totalAmount"`
}

func (s *OrderApplicationService) CreateOrder(
    req CreateOrderRequest) (*CreateOrderResponse, error) {
    
    var orderItems []order.OrderItem
    for _, item := range req.Items {
        orderItems = append(orderItems,
            order.NewOrderItem(
                item.ProductID,
                item.Quantity,
                order.NewMoney(item.Price, "USD"),
            ),
        )
    }

    o, err := s.orderService.CreateOrder(req.CustomerID,
        orderItems)
    if err != nil {
        return nil, err
    }

    return &CreateOrderResponse{
        OrderID:     o.ID,
        TotalAmount: o.TotalAmount.Amount,
    }, nil
}

5. Interface Layer

internal/infrastructure/http/rest/handler.go:

package rest

import (
    "encoding/json"
    "net/http"
    
    "ecommerce/internal/application"
)

type OrderHandler struct {
    appService *application.OrderApplicationService
}

func NewOrderHandler(
    appService *application.OrderApplicationService) *OrderHandler {
    return &OrderHandler{
        appService: appService,
    }
}

func (h *OrderHandler) CreateOrder(w http.ResponseWriter,
    r *http.Request) {
    var req application.CreateOrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    resp, err := h.appService.CreateOrder(req)
    if err != nil {
        http.Error(w, err.Error(),
            http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

Penjelasan

  1. Domain Layer

    • Entities
    • Value Objects
    • Aggregates
    • Domain Services
    • Repository Interfaces
  2. Infrastructure Layer

    • Repository Implementations
    • External Services
    • Database Access
    • HTTP Handlers
  3. Application Layer

    • Use Cases
    • Application Services
    • DTOs
    • Validation

Best Practices

  1. Domain Model

    • Keep domain logic pure
    • Use value objects
    • Enforce invariants
    • Rich domain model
  2. Layering

    • Clear separation
    • Dependency inversion
    • Clean interfaces
    • Single responsibility
  3. Testing

    • Unit tests for domain
    • Integration tests
    • Use mocks appropriately
    • Test business rules

Contoh Penggunaan

  1. Create Order:
curl -X POST http://localhost:8080/orders \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "cust_123",
    "items": [
      {
        "productId": "prod_1",
        "quantity": 2,
        "price": 29.99
      }
    ]
  }'

Response:

{
  "orderId": "ord_1234567890",
  "totalAmount": 59.98
}

Tips

  1. Design

    • Start with domain model
    • Use ubiquitous language
    • Define boundaries
    • Document decisions
  2. Implementation

    • Keep it simple
    • Use interfaces
    • Handle errors properly
    • Follow SOLID principles
  3. Maintenance

    • Regular refactoring
    • Update documentation
    • Monitor performance
    • Get feedback

Troubleshooting

  1. Common Issues

    • Circular dependencies
    • Leaky abstractions
    • Complex aggregates
    • Performance bottlenecks
  2. Solutions

    • Review boundaries
    • Simplify design
    • Use events
    • Profile code

Security

  1. Data Protection

    • Input validation
    • Authorization
    • Encryption
    • Audit logging
  2. Access Control

    • Role-based access
    • Authentication
    • API security
    • Rate limiting