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:
- Implement Clean Architecture
- Separate concerns
- Manage dependencies
- 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
Layers
- Core (Domain, Ports, Services)
- Infrastructure (Repositories, Handlers)
- Interface (HTTP, CLI)
Dependencies
- Inward dependencies
- Dependency injection
- Interface segregation
Business Logic
- Domain-centric
- Use cases
- Error handling
Best Practices
Architecture
- Dependency rule
- Clean boundaries
- SOLID principles
- Interface-based design
Code Organization
- Package structure
- Clear naming
- Consistent patterns
- Error handling
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
Design
- Start with domain
- Define interfaces
- Keep it simple
- Document decisions
Implementation
- Use dependency injection
- Handle errors properly
- Write tests first
- Monitor performance
Maintenance
- Regular refactoring
- Update documentation
- Monitor metrics
- Get feedback
Troubleshooting
Common Issues
- Circular dependencies
- Interface pollution
- Complex dependencies
- Business logic leaks
Solutions
- Review architecture
- Simplify interfaces
- Use composition
- Refactor code
Security
Data Protection
- Input validation
- Password hashing
- Access control
- Audit logging
API Security
- Authentication
- Authorization
- Rate limiting
- CORS policy