Clean Architecture

Clean Architecture adalah pendekatan untuk membangun aplikasi yang maintainable, testable, dan scalable. Tutorial ini akan menjelaskan implementasi Clean Architecture di Go.

Contoh Masalah

Bagaimana cara:

  1. Implement Clean Architecture
  2. Separate concerns
  3. Manage dependencies
  4. Handle business logic

Penyelesaian

1. Struktur Project

cleanapp/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── core/
│   │   ├── domain/
│   │   │   └── user.go
│   │   ├── ports/
│   │   │   ├── repositories.go
│   │   │   └── services.go
│   │   └── services/
│   │       └── user_service.go
│   ├── handlers/
│   │   └── http/
│   │       └── user_handler.go
│   ├── repositories/
│   │   └── postgres/
│   │       └── user_repository.go
│   └── server/
│       └── server.go
└── pkg/
    ├── logger/
    │   └── logger.go
    └── validator/
        └── validator.go

2. Core Domain

internal/core/domain/user.go:

package domain

import (
    "time"
    "errors"
)

var (
    ErrInvalidUser     = errors.New("invalid user")
    ErrUserNotFound    = errors.New("user not found")
    ErrDuplicateEmail  = errors.New("email already exists")
)

type User struct {
    ID        string
    Name      string
    Email     string
    Password  string
    CreatedAt time.Time
    UpdatedAt time.Time
}

func NewUser(name, email, password string) (*User, error) {
    if name == "" || email == "" || password == "" {
        return nil, ErrInvalidUser
    }

    return &User{
        ID:        generateID(),
        Name:      name,
        Email:     email,
        Password:  hashPassword(password),
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
    }, nil
}

func (u *User) Update(name, email string) error {
    if name == "" || email == "" {
        return ErrInvalidUser
    }

    u.Name = name
    u.Email = email
    u.UpdatedAt = time.Now()
    return nil
}

3. Ports (Interfaces)

internal/core/ports/repositories.go:

package ports

import "cleanapp/internal/core/domain"

type UserRepository interface {
    Save(user *domain.User) error
    FindByID(id string) (*domain.User, error)
    FindByEmail(email string) (*domain.User, error)
    Update(user *domain.User) error
    Delete(id string) error
}

internal/core/ports/services.go:

package ports

import "cleanapp/internal/core/domain"

type UserService interface {
    CreateUser(name, email, password string) (*domain.User, error)
    GetUser(id string) (*domain.User, error)
    UpdateUser(id, name, email string) (*domain.User, error)
    DeleteUser(id string) error
}

4. Services (Use Cases)

internal/core/services/user_service.go:

package services

import (
    "cleanapp/internal/core/domain"
    "cleanapp/internal/core/ports"
)

type userService struct {
    userRepo ports.UserRepository
}

func NewUserService(userRepo ports.UserRepository) ports.UserService {
    return &userService{
        userRepo: userRepo,
    }
}

func (s *userService) CreateUser(name, email,
    password string) (*domain.User, error) {
    // Check if email exists
    existing, err := s.userRepo.FindByEmail(email)
    if err == nil && existing != nil {
        return nil, domain.ErrDuplicateEmail
    }

    // Create new user
    user, err := domain.NewUser(name, email, password)
    if err != nil {
        return nil, err
    }

    // Save user
    if err := s.userRepo.Save(user); err != nil {
        return nil, err
    }

    return user, nil
}

func (s *userService) GetUser(id string) (*domain.User, error) {
    return s.userRepo.FindByID(id)
}

func (s *userService) UpdateUser(id, name,
    email string) (*domain.User, error) {
    // Get existing user
    user, err := s.userRepo.FindByID(id)
    if err != nil {
        return nil, err
    }

    // Check email uniqueness
    if email != user.Email {
        existing, err := s.userRepo.FindByEmail(email)
        if err == nil && existing != nil {
            return nil, domain.ErrDuplicateEmail
        }
    }

    // Update user
    if err := user.Update(name, email); err != nil {
        return nil, err
    }

    // Save changes
    if err := s.userRepo.Update(user); err != nil {
        return nil, err
    }

    return user, nil
}

func (s *userService) DeleteUser(id string) error {
    return s.userRepo.Delete(id)
}

5. Repositories

internal/repositories/postgres/user_repository.go:

package postgres

import (
    "database/sql"
    "cleanapp/internal/core/domain"
)

type userRepository struct {
    db *sql.DB
}

func NewUserRepository(db *sql.DB) *userRepository {
    return &userRepository{
        db: db,
    }
}

func (r *userRepository) Save(user *domain.User) error {
    query := `
        INSERT INTO users (
            id, name, email, password, created_at, updated_at
        ) VALUES ($1, $2, $3, $4, $5, $6)
    `

    _, err := r.db.Exec(query,
        user.ID,
        user.Name,
        user.Email,
        user.Password,
        user.CreatedAt,
        user.UpdatedAt,
    )

    return err
}

func (r *userRepository) FindByID(id string) (*domain.User,
    error) {
    query := `
        SELECT id, name, email, password, created_at, updated_at
        FROM users
        WHERE id = $1
    `

    var user domain.User
    err := r.db.QueryRow(query, id).Scan(
        &user.ID,
        &user.Name,
        &user.Email,
        &user.Password,
        &user.CreatedAt,
        &user.UpdatedAt,
    )

    if err == sql.ErrNoRows {
        return nil, domain.ErrUserNotFound
    }

    return &user, err
}

func (r *userRepository) FindByEmail(email string) (*domain.User,
    error) {
    query := `
        SELECT id, name, email, password, created_at, updated_at
        FROM users
        WHERE email = $1
    `

    var user domain.User
    err := r.db.QueryRow(query, email).Scan(
        &user.ID,
        &user.Name,
        &user.Email,
        &user.Password,
        &user.CreatedAt,
        &user.UpdatedAt,
    )

    if err == sql.ErrNoRows {
        return nil, nil
    }

    return &user, err
}

6. HTTP Handlers

internal/handlers/http/user_handler.go:

package http

import (
    "encoding/json"
    "net/http"
    
    "cleanapp/internal/core/domain"
    "cleanapp/internal/core/ports"
)

type UserHandler struct {
    userService ports.UserService
}

func NewUserHandler(userService ports.UserService) *UserHandler {
    return &UserHandler{
        userService: userService,
    }
}

type createUserRequest struct {
    Name     string `json:"name"`
    Email    string `json:"email"`
    Password string `json:"password"`
}

type userResponse struct {
    ID        string `json:"id"`
    Name      string `json:"name"`
    Email     string `json:"email"`
    CreatedAt string `json:"createdAt"`
}

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

    user, err := h.userService.CreateUser(req.Name, req.Email,
        req.Password)
    if err != nil {
        switch err {
        case domain.ErrInvalidUser:
            http.Error(w, err.Error(), http.StatusBadRequest)
        case domain.ErrDuplicateEmail:
            http.Error(w, err.Error(), http.StatusConflict)
        default:
            http.Error(w, "Internal server error",
                http.StatusInternalServerError)
        }
        return
    }

    resp := userResponse{
        ID:        user.ID,
        Name:      user.Name,
        Email:     user.Email,
        CreatedAt: user.CreatedAt.Format(time.RFC3339),
    }

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

7. Server Setup

internal/server/server.go:

package server

import (
    "net/http"
    
    "cleanapp/internal/handlers/http"
    "cleanapp/pkg/logger"
)

type Server struct {
    userHandler *http.UserHandler
    logger      logger.Logger
}

func NewServer(userHandler *http.UserHandler,
    logger logger.Logger) *Server {
    return &Server{
        userHandler: userHandler,
        logger:      logger,
    }
}

func (s *Server) SetupRoutes() http.Handler {
    mux := http.NewServeMux()

    // User routes
    mux.HandleFunc("/users", s.userHandler.CreateUser)

    // Add middleware
    handler := logger.LoggingMiddleware(mux)
    handler = corsMiddleware(handler)
    handler = recoveryMiddleware(handler)

    return handler
}

8. Main Application

cmd/api/main.go:

package main

import (
    "database/sql"
    "log"
    "net/http"
    
    "cleanapp/internal/core/services"
    "cleanapp/internal/handlers/http"
    "cleanapp/internal/repositories/postgres"
    "cleanapp/internal/server"
    "cleanapp/pkg/logger"
)

func main() {
    // Setup database
    db, err := sql.Open("postgres",
        "postgres://user:pass@localhost/dbname?sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Setup logger
    logger := logger.NewLogger()

    // Setup repositories
    userRepo := postgres.NewUserRepository(db)

    // Setup services
    userService := services.NewUserService(userRepo)

    // Setup handlers
    userHandler := http.NewUserHandler(userService)

    // Setup server
    srv := server.NewServer(userHandler, logger)
    handler := srv.SetupRoutes()

    // Start server
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", handler))
}

Penjelasan

  1. Layers

    • Core (Domain, Ports, Services)
    • Infrastructure (Repositories, Handlers)
    • Interface (HTTP, CLI)
  2. Dependencies

    • Inward dependencies
    • Dependency injection
    • Interface segregation
  3. Business Logic

    • Domain-centric
    • Use cases
    • Error handling

Best Practices

  1. Architecture

    • Dependency rule
    • Clean boundaries
    • SOLID principles
    • Interface-based design
  2. Code Organization

    • Package structure
    • Clear naming
    • Consistent patterns
    • Error handling
  3. Testing

    • Unit tests
    • Integration tests
    • Mocking
    • Test coverage

Contoh Penggunaan

Create user:

curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Doe",
    "email": "john@example.com",
    "password": "secret123"
  }'

Response:

{
  "id": "usr_1234567890",
  "name": "John Doe",
  "email": "john@example.com",
  "createdAt": "2024-01-21T12:26:07Z"
}

Tips

  1. Design

    • Start with domain
    • Define interfaces
    • Keep it simple
    • Document decisions
  2. Implementation

    • Use dependency injection
    • Handle errors properly
    • Write tests first
    • Monitor performance
  3. Maintenance

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

Troubleshooting

  1. Common Issues

    • Circular dependencies
    • Interface pollution
    • Complex dependencies
    • Business logic leaks
  2. Solutions

    • Review architecture
    • Simplify interfaces
    • Use composition
    • Refactor code

Security

  1. Data Protection

    • Input validation
    • Password hashing
    • Access control
    • Audit logging
  2. API Security

    • Authentication
    • Authorization
    • Rate limiting
    • CORS policy