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:
- Implement DDD patterns
- Structure domain model
- Handle business rules
- 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
Domain Layer
- Entities
- Value Objects
- Aggregates
- Domain Services
- Repository Interfaces
Infrastructure Layer
- Repository Implementations
- External Services
- Database Access
- HTTP Handlers
Application Layer
- Use Cases
- Application Services
- DTOs
- Validation
Best Practices
Domain Model
- Keep domain logic pure
- Use value objects
- Enforce invariants
- Rich domain model
Layering
- Clear separation
- Dependency inversion
- Clean interfaces
- Single responsibility
Testing
- Unit tests for domain
- Integration tests
- Use mocks appropriately
- Test business rules
Contoh Penggunaan
- 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
Design
- Start with domain model
- Use ubiquitous language
- Define boundaries
- Document decisions
Implementation
- Keep it simple
- Use interfaces
- Handle errors properly
- Follow SOLID principles
Maintenance
- Regular refactoring
- Update documentation
- Monitor performance
- Get feedback
Troubleshooting
Common Issues
- Circular dependencies
- Leaky abstractions
- Complex aggregates
- Performance bottlenecks
Solutions
- Review boundaries
- Simplify design
- Use events
- Profile code
Security
Data Protection
- Input validation
- Authorization
- Encryption
- Audit logging
Access Control
- Role-based access
- Authentication
- API security
- Rate limiting