The Complete Guide to Clean Architecture
Table of Contents
- Introduction to Clean Architecture
- Core Principles and Rules
- Layer-by-Layer Breakdown
- Sample Project: User Management System
- Complete Go Implementation
- Testing Strategy
- Common Patterns and Practices
- Pitfalls and How to Avoid Them
- Real-World Considerations
Introduction to Clean Architecture {#introduction}
What is Clean Architecture?
Clean Architecture, introduced by Robert C. Martin (Uncle Bob), is an architectural pattern that separates software into layers with strict dependency rules. The goal is to create systems that are:
- Independent of frameworks: Your business logic doesn’t depend on external libraries
- Testable: Business rules can be tested without UI, database, or external services
- Independent of UI: You can swap UIs without changing business logic
- Independent of database: Business rules don’t know about the database
- Independent of external agencies: Business rules don’t know about the outside world
The Dependency Rule
The fundamental rule: Source code dependencies must point only inward, toward higher-level policies.
┌─────────────────────────────────────────┐
│ Frameworks & Drivers │ External
│ (Web, DB, UI, External Services) │ Layer
├─────────────────────────────────────────┤
│ Interface Adapters │ Adapters
│ (Controllers, Presenters, Gateways) │ Layer
├─────────────────────────────────────────┤
│ Use Cases │ Application
│ (Application Business Rules) │ Layer
├─────────────────────────────────────────┤
│ Entities │ Enterprise
│ (Enterprise Business Rules) │ Layer
└─────────────────────────────────────────┘
↑ ↑ ↑ ↑
Dependencies point INWARD only
Key insight: Inner layers define interfaces; outer layers implement them. This inverts the typical dependency flow.
Why Clean Architecture?
Traditional layered architecture problems:
// ❌ Traditional approach - tight coupling
type UserService struct {
db *sql.DB // Depends on concrete implementation
}
func (s *UserService) CreateUser(name string) error {
_, err := s.db.Exec("INSERT INTO users...") // SQL in business logic
return err
}
Clean Architecture solution:
// ✅ Clean Architecture - dependency inversion
type UserRepository interface {
Save(user *User) error
}
type UserService struct {
repo UserRepository // Depends on abstraction
}
func (s *UserService) CreateUser(name string) error {
user := &User{Name: name}
return s.repo.Save(user) // No knowledge of database
}
Core Principles and Rules {#principles}
1. The Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
// ❌ Bad: High-level depends on low-level
type OrderProcessor struct {
mysql *MySQLDatabase
}
// ✅ Good: Both depend on abstraction
type OrderRepository interface {
Save(order *Order) error
}
type OrderProcessor struct {
repo OrderRepository
}
type MySQLOrderRepository struct {
db *sql.DB
}
func (r *MySQLOrderRepository) Save(order *Order) error {
// Implementation
}
2. The Stable Abstractions Principle
Abstractions (interfaces) should be more stable than implementations.
// Stable interface in domain layer
type PaymentGateway interface {
Charge(amount Money, card CreditCard) (TransactionID, error)
}
// Volatile implementation in infrastructure layer
type StripePaymentGateway struct {
apiKey string
client *stripe.Client
}
func (g *StripePaymentGateway) Charge(amount Money, card CreditCard) (TransactionID, error) {
// Stripe-specific implementation
}
3. Screaming Architecture
Your architecture should scream what the application does, not what frameworks it uses.
❌ Framework-centric:
myapp/
├── controllers/
├── models/
├── views/
└── routes/
✅ Domain-centric:
myapp/
├── users/
├── orders/
├── products/
├── payments/
└── shipping/
4. Separation of Concerns
Each layer has a specific responsibility:
- Entities: Enterprise business rules and data
- Use Cases: Application-specific business rules
- Interface Adapters: Convert data between use cases and external world
- Frameworks & Drivers: External tools and frameworks
Layer-by-Layer Breakdown {#layers}
Layer 1: Entities (Enterprise Business Rules)
Purpose: Core business objects and rules that are independent of any application.
Characteristics:
- Pure business logic
- No dependencies on outer layers
- Minimal dependencies on frameworks
- Highly reusable across applications
package domain
import (
"errors"
"time"
)
// User represents a user entity
type User struct {
ID string
Email string
Password string
CreatedAt time.Time
UpdatedAt time.Time
}
// Validate ensures user data is valid
func (u *User) Validate() error {
if u.Email == "" {
return errors.New("email is required")
}
if len(u.Password) < 8 {
return errors.New("password must be at least 8 characters")
}
return nil
}
// IsActive checks if user account is active
func (u *User) IsActive() bool {
// Business rule: users are active if created in last 365 days
return time.Since(u.CreatedAt) < 365*24*time.Hour
}
Layer 2: Use Cases (Application Business Rules)
Purpose: Application-specific business rules that orchestrate the flow of data to and from entities.
Characteristics:
- Define what the application does
- Orchestrate entities
- Define interfaces for data access
- Independent of UI, database, frameworks
package usecase
import (
"context"
"errors"
"myapp/domain"
)
// UserRepository defines data access interface
type UserRepository interface {
Save(ctx context.Context, user *domain.User) error
FindByEmail(ctx context.Context, email string) (*domain.User, error)
FindByID(ctx context.Context, id string) (*domain.User, error)
}
// PasswordHasher defines password hashing interface
type PasswordHasher interface {
Hash(password string) (string, error)
Compare(hashedPassword, password string) error
}
// CreateUserUseCase handles user creation
type CreateUserUseCase struct {
userRepo UserRepository
hasher PasswordHasher
}
func NewCreateUserUseCase(repo UserRepository, hasher PasswordHasher) *CreateUserUseCase {
return &CreateUserUseCase{
userRepo: repo,
hasher: hasher,
}
}
func (uc *CreateUserUseCase) Execute(ctx context.Context, email, password string) (*domain.User, error) {
// Check if user already exists
existing, err := uc.userRepo.FindByEmail(ctx, email)
if err == nil && existing != nil {
return nil, errors.New("user already exists")
}
// Create new user
user := &domain.User{
Email: email,
Password: password,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Validate business rules
if err := user.Validate(); err != nil {
return nil, err
}
// Hash password
hashedPassword, err := uc.hasher.Hash(password)
if err != nil {
return nil, err
}
user.Password = hashedPassword
// Save user
if err := uc.userRepo.Save(ctx, user); err != nil {
return nil, err
}
return user, nil
}
Layer 3: Interface Adapters
Purpose: Convert data between the format most convenient for use cases and entities, and the format most convenient for external agencies.
Characteristics:
- Controllers, presenters, gateways
- Implement interfaces defined by use cases
- Convert data formats
- Handle framework-specific concerns
package repository
import (
"context"
"database/sql"
"myapp/domain"
)
// PostgresUserRepository implements UserRepository using PostgreSQL
type PostgresUserRepository struct {
db *sql.DB
}
func NewPostgresUserRepository(db *sql.DB) *PostgresUserRepository {
return &PostgresUserRepository{db: db}
}
func (r *PostgresUserRepository) Save(ctx context.Context, user *domain.User) error {
query := `
INSERT INTO users (id, email, password, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5)
`
_, err := r.db.ExecContext(ctx, query,
user.ID, user.Email, user.Password, user.CreatedAt, user.UpdatedAt)
return err
}
func (r *PostgresUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
query := `SELECT id, email, password, created_at, updated_at FROM users WHERE email = $1`
user := &domain.User{}
err := r.db.QueryRowContext(ctx, query, email).Scan(
&user.ID, &user.Email, &user.Password, &user.CreatedAt, &user.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return user, nil
}
Layer 4: Frameworks & Drivers
Purpose: External frameworks and tools (web frameworks, databases, etc.)
Characteristics:
- Outermost layer
- Implementation details
- Easily swappable
- Main entry point and dependency wiring
package main
import (
"database/sql"
"log"
"net/http"
"myapp/handler"
"myapp/repository"
"myapp/security"
"myapp/usecase"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
)
func main() {
// Database connection (framework layer)
db, err := sql.Open("postgres", "postgres://...")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Initialize repositories (adapter layer)
userRepo := repository.NewPostgresUserRepository(db)
hasher := security.NewBcryptHasher()
// Initialize use cases (application layer)
createUserUC := usecase.NewCreateUserUseCase(userRepo, hasher)
// Initialize handlers (adapter layer)
userHandler := handler.NewUserHandler(createUserUC)
// Setup HTTP router (framework layer)
router := mux.NewRouter()
router.HandleFunc("/users", userHandler.CreateUser).Methods("POST")
// Start server
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", router))
}
Sample Project: User Management System {#sample-project}
We’ll build a complete user management system with the following features:
- User registration
- User authentication
- Profile management
- Role-based access control
Project Structure
user-management/
├── cmd/
│ └── api/
│ └── main.go # Application entry point
├── internal/
│ ├── domain/ # Enterprise business rules
│ │ ├── user.go
│ │ ├── role.go
│ │ └── errors.go
│ ├── usecase/ # Application business rules
│ │ ├── user_registration.go
│ │ ├── user_authentication.go
│ │ ├── user_profile.go
│ │ └── interfaces.go
│ ├── repository/ # Data access adapters
│ │ ├── postgres/
│ │ │ ├── user_repository.go
│ │ │ └── role_repository.go
│ │ └── memory/
│ │ └── user_repository.go
│ ├── handler/ # HTTP handlers
│ │ ├── http/
│ │ │ ├── user_handler.go
│ │ │ ├── auth_handler.go
│ │ │ └── middleware.go
│ │ └── dto/
│ │ ├── user_dto.go
│ │ └── auth_dto.go
│ ├── security/ # Security adapters
│ │ ├── hasher.go
│ │ └── jwt.go
│ └── config/ # Configuration
│ └── config.go
├── pkg/ # Shared utilities
│ ├── validator/
│ │ └── validator.go
│ └── logger/
│ └── logger.go
├── migrations/ # Database migrations
│ ├── 001_create_users_table.sql
│ └── 002_create_roles_table.sql
├── go.mod
└── go.sum
Complete Go Implementation {#implementation}
Domain Layer
internal/domain/user.go:
package domain
import (
"errors"
"regexp"
"time"
)
var (
ErrInvalidEmail = errors.New("invalid email format")
ErrPasswordTooShort = errors.New("password must be at least 8 characters")
ErrInvalidUserID = errors.New("invalid user ID")
)
// User represents a user in the system
type User struct {
ID string
Email string
PasswordHash string
FirstName string
LastName string
RoleID string
IsActive bool
CreatedAt time.Time
UpdatedAt time.Time
LastLoginAt *time.Time
}
// NewUser creates a new user with validation
func NewUser(email, firstName, lastName string) (*User, error) {
user := &User{
Email: email,
FirstName: firstName,
LastName: lastName,
IsActive: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := user.Validate(); err != nil {
return nil, err
}
return user, nil
}
// Validate checks if user data is valid
func (u *User) Validate() error {
if !isValidEmail(u.Email) {
return ErrInvalidEmail
}
if u.FirstName == "" || u.LastName == "" {
return errors.New("first name and last name are required")
}
return nil
}
// FullName returns the user's full name
func (u *User) FullName() string {
return u.FirstName + " " + u.LastName
}
// RecordLogin updates the last login time
func (u *User) RecordLogin() {
now := time.Now()
u.LastLoginAt = &now
u.UpdatedAt = now
}
// Deactivate marks the user as inactive
func (u *User) Deactivate() {
u.IsActive = false
u.UpdatedAt = time.Now()
}
// Activate marks the user as active
func (u *User) Activate() {
u.IsActive = true
u.UpdatedAt = time.Now()
}
func isValidEmail(email string) bool {
emailRegex := regexp.MustCompile(\`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$\`)
return emailRegex.MatchString(email)
}
internal/domain/role.go:
package domain
import "time"
// Permission represents a permission in the system
type Permission string
const (
PermissionReadUsers Permission = "users:read"
PermissionWriteUsers Permission = "users:write"
PermissionDeleteUsers Permission = "users:delete"
PermissionManageRoles Permission = "roles:manage"
)
// Role represents a user role
type Role struct {
ID string
Name string
Description string
Permissions []Permission
CreatedAt time.Time
UpdatedAt time.Time
}
// HasPermission checks if role has a specific permission
func (r *Role) HasPermission(permission Permission) bool {
for _, p := range r.Permissions {
if p == permission {
return true
}
}
return false
}
// Common roles
var (
RoleAdmin = &Role{
ID: "admin",
Name: "Administrator",
Permissions: []Permission{
PermissionReadUsers,
PermissionWriteUsers,
PermissionDeleteUsers,
PermissionManageRoles,
},
}
RoleUser = &Role{
ID: "user",
Name: "User",
Permissions: []Permission{
PermissionReadUsers,
},
}
)
internal/domain/errors.go:
package domain
import "errors"
var (
// User errors
ErrUserNotFound = errors.New("user not found")
ErrUserAlreadyExists = errors.New("user already exists")
ErrInvalidCredentials = errors.New("invalid credentials")
ErrUserNotActive = errors.New("user account is not active")
// Role errors
ErrRoleNotFound = errors.New("role not found")
ErrUnauthorized = errors.New("unauthorized")
// General errors
ErrInvalidInput = errors.New("invalid input")
)
Use Case Layer
internal/usecase/interfaces.go:
package usecase
import (
"context"
"myapp/internal/domain"
)
// UserRepository defines the interface for user data access
type UserRepository interface {
Save(ctx context.Context, user *domain.User) error
FindByID(ctx context.Context, id string) (*domain.User, error)
FindByEmail(ctx context.Context, email string) (*domain.User, error)
Update(ctx context.Context, user *domain.User) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, limit, offset int) ([]*domain.User, error)
}
// RoleRepository defines the interface for role data access
type RoleRepository interface {
FindByID(ctx context.Context, id string) (*domain.Role, error)
FindByName(ctx context.Context, name string) (*domain.Role, error)
List(ctx context.Context) ([]*domain.Role, error)
}
// PasswordHasher defines the interface for password hashing
type PasswordHasher interface {
Hash(password string) (string, error)
Compare(hashedPassword, password string) error
}
// TokenGenerator defines the interface for token generation
type TokenGenerator interface {
Generate(userID string, email string) (string, error)
Validate(token string) (userID string, err error)
}
// IDGenerator defines the interface for ID generation
type IDGenerator interface {
Generate() string
}
internal/usecase/user_registration.go:
package usecase
import (
"context"
"myapp/internal/domain"
)
// RegisterUserRequest represents user registration input
type RegisterUserRequest struct {
Email string
Password string
FirstName string
LastName string
}
// RegisterUserResponse represents user registration output
type RegisterUserResponse struct {
UserID string
Email string
}
// UserRegistrationUseCase handles user registration
type UserRegistrationUseCase struct {
userRepo UserRepository
roleRepo RoleRepository
hasher PasswordHasher
idGen IDGenerator
}
// NewUserRegistrationUseCase creates a new user registration use case
func NewUserRegistrationUseCase(
userRepo UserRepository,
roleRepo RoleRepository,
hasher PasswordHasher,
idGen IDGenerator,
) *UserRegistrationUseCase {
return &UserRegistrationUseCase{
userRepo: userRepo,
roleRepo: roleRepo,
hasher: hasher,
idGen: idGen,
}
}
// Execute registers a new user
func (uc *UserRegistrationUseCase) Execute(ctx context.Context, req RegisterUserRequest) (*RegisterUserResponse, error) {
// Validate password length
if len(req.Password) < 8 {
return nil, domain.ErrPasswordTooShort
}
// Check if user already exists
existingUser, err := uc.userRepo.FindByEmail(ctx, req.Email)
if err != nil && err != domain.ErrUserNotFound {
return nil, err
}
if existingUser != nil {
return nil, domain.ErrUserAlreadyExists
}
// Create new user
user, err := domain.NewUser(req.Email, req.FirstName, req.LastName)
if err != nil {
return nil, err
}
// Generate unique ID
user.ID = uc.idGen.Generate()
// Hash password
hashedPassword, err := uc.hasher.Hash(req.Password)
if err != nil {
return nil, err
}
user.PasswordHash = hashedPassword
// Assign default role
defaultRole, err := uc.roleRepo.FindByName(ctx, "user")
if err != nil {
return nil, err
}
user.RoleID = defaultRole.ID
// Save user
if err := uc.userRepo.Save(ctx, user); err != nil {
return nil, err
}
return &RegisterUserResponse{
UserID: user.ID,
Email: user.Email,
}, nil
}
internal/usecase/user_authentication.go:
package usecase
import (
"context"
"myapp/internal/domain"
)
// LoginRequest represents login input
type LoginRequest struct {
Email string
Password string
}
// LoginResponse represents login output
type LoginResponse struct {
Token string
UserID string
Email string
}
// UserAuthenticationUseCase handles user authentication
type UserAuthenticationUseCase struct {
userRepo UserRepository
hasher PasswordHasher
tokenGen TokenGenerator
}
// NewUserAuthenticationUseCase creates a new authentication use case
func NewUserAuthenticationUseCase(
userRepo UserRepository,
hasher PasswordHasher,
tokenGen TokenGenerator,
) *UserAuthenticationUseCase {
return &UserAuthenticationUseCase{
userRepo: userRepo,
hasher: hasher,
tokenGen: tokenGen,
}
}
// Execute authenticates a user and returns a token
func (uc *UserAuthenticationUseCase) Execute(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
// Find user by email
user, err := uc.userRepo.FindByEmail(ctx, req.Email)
if err != nil {
if err == domain.ErrUserNotFound {
return nil, domain.ErrInvalidCredentials
}
return nil, err
}
// Check if user is active
if !user.IsActive {
return nil, domain.ErrUserNotActive
}
// Verify password
if err := uc.hasher.Compare(user.PasswordHash, req.Password); err != nil {
return nil, domain.ErrInvalidCredentials
}
// Generate token
token, err := uc.tokenGen.Generate(user.ID, user.Email)
if err != nil {
return nil, err
}
// Record login
user.RecordLogin()
if err := uc.userRepo.Update(ctx, user); err != nil {
// Log error but don't fail the login
// This is a non-critical operation
}
return &LoginResponse{
Token: token,
UserID: user.ID,
Email: user.Email,
}, nil
}
internal/usecase/user_profile.go:
package usecase
import (
"context"
"myapp/internal/domain"
)
// GetUserProfileRequest represents get profile input
type GetUserProfileRequest struct {
UserID string
RequestUserID string // User making the request
}
// UserProfile represents user profile output
type UserProfile struct {
ID string
Email string
FirstName string
LastName string
FullName string
IsActive bool
Role string
CreatedAt string
LastLoginAt *string
}
// UpdateUserProfileRequest represents update profile input
type UpdateUserProfileRequest struct {
UserID string
FirstName string
LastName string
}
// UserProfileUseCase handles user profile operations
type UserProfileUseCase struct {
userRepo UserRepository
roleRepo RoleRepository
}
// NewUserProfileUseCase creates a new user profile use case
func NewUserProfileUseCase(userRepo UserRepository, roleRepo RoleRepository) *UserProfileUseCase {
return &UserProfileUseCase{
userRepo: userRepo,
roleRepo: roleRepo,
}
}
// GetProfile retrieves a user's profile
func (uc *UserProfileUseCase) GetProfile(ctx context.Context, req GetUserProfileRequest) (*UserProfile, error) {
// Find user
user, err := uc.userRepo.FindByID(ctx, req.UserID)
if err != nil {
return nil, err
}
// Get role information
role, err := uc.roleRepo.FindByID(ctx, user.RoleID)
if err != nil {
return nil, err
}
// Build profile
profile := &UserProfile{
ID: user.ID,
Email: user.Email,
FirstName: user.FirstName,
LastName: user.LastName,
FullName: user.FullName(),
IsActive: user.IsActive,
Role: role.Name,
CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z"),
}
if user.LastLoginAt != nil {
lastLogin := user.LastLoginAt.Format("2006-01-02T15:04:05Z")
profile.LastLoginAt = &lastLogin
}
return profile, nil
}
// UpdateProfile updates a user's profile
func (uc *UserProfileUseCase) UpdateProfile(ctx context.Context, req UpdateUserProfileRequest) error {
// Find user
user, err := uc.userRepo.FindByID(ctx, req.UserID)
if err != nil {
return err
}
// Update fields
if req.FirstName != "" {
user.FirstName = req.FirstName
}
if req.LastName != "" {
user.LastName = req.LastName
}
// Validate
if err := user.Validate(); err != nil {
return err
}
// Save changes
return uc.userRepo.Update(ctx, user)
}
Repository Layer (Interface Adapters)
internal/repository/postgres/user_repository.go:
package postgres
import (
"context"
"database/sql"
"myapp/internal/domain"
"time"
)
// UserRepository implements the UserRepository interface using PostgreSQL
type UserRepository struct {
db *sql.DB
}
// NewUserRepository creates a new PostgreSQL user repository
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
// Save creates a new user in the database
func (r *UserRepository) Save(ctx context.Context, user *domain.User) error {
query := `
INSERT INTO users (id, email, password_hash, first_name, last_name, role_id, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`
_, err := r.db.ExecContext(ctx, query,
user.ID,
user.Email,
user.PasswordHash,
user.FirstName,
user.LastName,
user.RoleID,
user.IsActive,
user.CreatedAt,
user.UpdatedAt,
)
return err
}
// FindByID retrieves a user by ID
func (r *UserRepository) FindByID(ctx context.Context, id string) (*domain.User, error) {
query := `
SELECT id, email, password_hash, first_name, last_name, role_id, is_active,
created_at, updated_at, last_login_at
FROM users
WHERE id = $1
`
user := &domain.User{}
var lastLoginAt sql.NullTime
err := r.db.QueryRowContext(ctx, query, id).Scan(
&user.ID,
&user.Email,
&user.PasswordHash,
&user.FirstName,
&user.LastName,
&user.RoleID,
&user.IsActive,
&user.CreatedAt,
&user.UpdatedAt,
&lastLoginAt,
)
if err == sql.ErrNoRows {
return nil, domain.ErrUserNotFound
}
if err != nil {
return nil, err
}
if lastLoginAt.Valid {
user.LastLoginAt = &lastLoginAt.Time
}
return user, nil
}
// FindByEmail retrieves a user by email
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
query := `
SELECT id, email, password_hash, first_name, last_name, role_id, is_active,
created_at, updated_at, last_login_at
FROM users
WHERE email = $1
`
user := &domain.User{}
var lastLoginAt sql.NullTime
err := r.db.QueryRowContext(ctx, query, email).Scan(
&user.ID,
&user.Email,
&user.PasswordHash,
&user.FirstName,
&user.LastName,
&user.RoleID,
&user.IsActive,
&user.CreatedAt,
&user.UpdatedAt,
&lastLoginAt,
)
if err == sql.ErrNoRows {
return nil, domain.ErrUserNotFound
}
if err != nil {
return nil, err
}
if lastLoginAt.Valid {
user.LastLoginAt = &lastLoginAt.Time
}
return user, nil
}
// Update updates an existing user
func (r *UserRepository) Update(ctx context.Context, user *domain.User) error {
query := `
UPDATE users
SET email = $2, password_hash = $3, first_name = $4, last_name = $5,
role_id = $6, is_active = $7, updated_at = $8, last_login_at = $9
WHERE id = $1
`
user.UpdatedAt = time.Now()
_, err := r.db.ExecContext(ctx, query,
user.ID,
user.Email,
user.PasswordHash,
user.FirstName,
user.LastName,
user.RoleID,
user.IsActive,
user.UpdatedAt,
user.LastLoginAt,
)
return err
}
// Delete removes a user from the database
func (r *UserRepository) Delete(ctx context.Context, id string) error {
query := `DELETE FROM users WHERE id = $1`
_, err := r.db.ExecContext(ctx, query, id)
return err
}
// List retrieves a paginated list of users
func (r *UserRepository) List(ctx context.Context, limit, offset int) ([]*domain.User, error) {
query := `
SELECT id, email, password_hash, first_name, last_name, role_id, is_active,
created_at, updated_at, last_login_at
FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`
rows, err := r.db.QueryContext(ctx, query, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
var users []*domain.User
for rows.Next() {
user := &domain.User{}
var lastLoginAt sql.NullTime
err := rows.Scan(
&user.ID,
&user.Email,
&user.PasswordHash,
&user.FirstName,
&user.LastName,
&user.RoleID,
&user.IsActive,
&user.CreatedAt,
&user.UpdatedAt,
&lastLoginAt,
)
if err != nil {
return nil, err
}
if lastLoginAt.Valid {
user.LastLoginAt = &lastLoginAt.Time
}
users = append(users, user)
}
return users, rows.Err()
}
internal/repository/postgres/role_repository.go:
package postgres
import (
"context"
"database/sql"
"encoding/json"
"myapp/internal/domain"
)
// RoleRepository implements the RoleRepository interface using PostgreSQL
type RoleRepository struct {
db *sql.DB
}
// NewRoleRepository creates a new PostgreSQL role repository
func NewRoleRepository(db *sql.DB) *RoleRepository {
return &RoleRepository{db: db}
}
// FindByID retrieves a role by ID
func (r *RoleRepository) FindByID(ctx context.Context, id string) (*domain.Role, error) {
query := `
SELECT id, name, description, permissions, created_at, updated_at
FROM roles
WHERE id = $1
`
role := &domain.Role{}
var permissionsJSON []byte
err := r.db.QueryRowContext(ctx, query, id).Scan(
&role.ID,
&role.Name,
&role.Description,
&permissionsJSON,
&role.CreatedAt,
&role.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, domain.ErrRoleNotFound
}
if err != nil {
return nil, err
}
if err := json.Unmarshal(permissionsJSON, &role.Permissions); err != nil {
return nil, err
}
return role, nil
}
// FindByName retrieves a role by name
func (r *RoleRepository) FindByName(ctx context.Context, name string) (*domain.Role, error) {
query := `
SELECT id, name, description, permissions, created_at, updated_at
FROM roles
WHERE name = $1
`
role := &domain.Role{}
var permissionsJSON []byte
err := r.db.QueryRowContext(ctx, query, name).Scan(
&role.ID,
&role.Name,
&role.Description,
&permissionsJSON,
&role.CreatedAt,
&role.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, domain.ErrRoleNotFound
}
if err != nil {
return nil, err
}
if err := json.Unmarshal(permissionsJSON, &role.Permissions); err != nil {
return nil, err
}
return role, nil
}
// List retrieves all roles
func (r *RoleRepository) List(ctx context.Context) ([]*domain.Role, error) {
query := `
SELECT id, name, description, permissions, created_at, updated_at
FROM roles
ORDER BY name
`
rows, err := r.db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var roles []*domain.Role
for rows.Next() {
role := &domain.Role{}
var permissionsJSON []byte
err := rows.Scan(
&role.ID,
&role.Name,
&role.Description,
&permissionsJSON,
&role.CreatedAt,
&role.UpdatedAt,
)
if err != nil {
return nil, err
}
if err := json.Unmarshal(permissionsJSON, &role.Permissions); err != nil {
return nil, err
}
roles = append(roles, role)
}
return roles, rows.Err()
}
Security Layer (Interface Adapters)
internal/security/hasher.go:
package security
import (
"golang.org/x/crypto/bcrypt"
)
// BcryptHasher implements password hashing using bcrypt
type BcryptHasher struct {
cost int
}
// NewBcryptHasher creates a new bcrypt hasher
func NewBcryptHasher() *BcryptHasher {
return &BcryptHasher{
cost: bcrypt.DefaultCost,
}
}
// Hash generates a bcrypt hash of the password
func (h *BcryptHasher) Hash(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), h.cost)
return string(bytes), err
}
// Compare compares a password with a hash
func (h *BcryptHasher) Compare(hashedPassword, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
internal/security/jwt.go:
package security
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
var (
ErrInvalidToken = errors.New("invalid token")
ErrExpiredToken = errors.New("token has expired")
)
// Claims represents JWT claims
type Claims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
// JWTGenerator implements token generation using JWT
type JWTGenerator struct {
secretKey []byte
tokenDuration time.Duration
}
// NewJWTGenerator creates a new JWT generator
func NewJWTGenerator(secretKey string, tokenDuration time.Duration) *JWTGenerator {
return &JWTGenerator{
secretKey: []byte(secretKey),
tokenDuration: tokenDuration,
}
}
// Generate creates a new JWT token
func (g *JWTGenerator) Generate(userID, email string) (string, error) {
claims := &Claims{
UserID: userID,
Email: email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(g.tokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(g.secretKey)
}
// Validate validates a JWT token and returns the user ID
func (g *JWTGenerator) Validate(tokenString string) (string, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return g.secretKey, nil
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return "", ErrExpiredToken
}
return "", ErrInvalidToken
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims.UserID, nil
}
return "", ErrInvalidToken
}
internal/security/id_generator.go:
package security
import "github.com/google/uuid"
// UUIDGenerator implements ID generation using UUID
type UUIDGenerator struct{}
// NewUUIDGenerator creates a new UUID generator
func NewUUIDGenerator() *UUIDGenerator {
return &UUIDGenerator{}
}
// Generate creates a new UUID
func (g *UUIDGenerator) Generate() string {
return uuid.New().String()
}
HTTP Handler Layer (Interface Adapters)
internal/handler/dto/user_dto.go:
package dto
// RegisterUserRequest represents user registration HTTP request
type RegisterUserRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
FirstName string `json:"first_name" validate:"required"`
LastName string `json:"last_name" validate:"required"`
}
// RegisterUserResponse represents user registration HTTP response
type RegisterUserResponse struct {
UserID string `json:"user_id"`
Email string `json:"email"`
}
// LoginRequest represents login HTTP request
type LoginRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
// LoginResponse represents login HTTP response
type LoginResponse struct {
Token string `json:"token"`
UserID string `json:"user_id"`
Email string `json:"email"`
}
// UpdateProfileRequest represents update profile HTTP request
type UpdateProfileRequest struct {
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
}
// UserProfileResponse represents user profile HTTP response
type UserProfileResponse struct {
ID string `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
FullName string `json:"full_name"`
IsActive bool `json:"is_active"`
Role string `json:"role"`
CreatedAt string `json:"created_at"`
LastLoginAt *string `json:"last_login_at,omitempty"`
}
// ErrorResponse represents an error HTTP response
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
}
internal/handler/http/user_handler.go:
package http
import (
"encoding/json"
"myapp/internal/handler/dto"
"myapp/internal/usecase"
"net/http"
"github.com/go-playground/validator/v10"
)
// UserHandler handles user-related HTTP requests
type UserHandler struct {
registerUC *usecase.UserRegistrationUseCase
loginUC *usecase.UserAuthenticationUseCase
profileUC *usecase.UserProfileUseCase
validator *validator.Validate
}
// NewUserHandler creates a new user handler
func NewUserHandler(
registerUC *usecase.UserRegistrationUseCase,
loginUC *usecase.UserAuthenticationUseCase,
profileUC *usecase.UserProfileUseCase,
) *UserHandler {
return &UserHandler{
registerUC: registerUC,
loginUC: loginUC,
profileUC: profileUC,
validator: validator.New(),
}
}
// Register handles user registration
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
var req dto.RegisterUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err)
return
}
if err := h.validator.Struct(req); err != nil {
respondError(w, http.StatusBadRequest, "Validation failed", err)
return
}
ucReq := usecase.RegisterUserRequest{
Email: req.Email,
Password: req.Password,
FirstName: req.FirstName,
LastName: req.LastName,
}
result, err := h.registerUC.Execute(r.Context(), ucReq)
if err != nil {
respondError(w, http.StatusBadRequest, "Registration failed", err)
return
}
response := dto.RegisterUserResponse{
UserID: result.UserID,
Email: result.Email,
}
respondJSON(w, http.StatusCreated, response)
}
// Login handles user login
func (h *UserHandler) Login(w http.ResponseWriter, r *http.Request) {
var req dto.LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err)
return
}
if err := h.validator.Struct(req); err != nil {
respondError(w, http.StatusBadRequest, "Validation failed", err)
return
}
ucReq := usecase.LoginRequest{
Email: req.Email,
Password: req.Password,
}
result, err := h.loginUC.Execute(r.Context(), ucReq)
if err != nil {
respondError(w, http.StatusUnauthorized, "Login failed", err)
return
}
response := dto.LoginResponse{
Token: result.Token,
UserID: result.UserID,
Email: result.Email,
}
respondJSON(w, http.StatusOK, response)
}
// GetProfile handles retrieving user profile
func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value("user_id").(string)
ucReq := usecase.GetUserProfileRequest{
UserID: userID,
RequestUserID: userID,
}
profile, err := h.profileUC.GetProfile(r.Context(), ucReq)
if err != nil {
respondError(w, http.StatusNotFound, "Profile not found", err)
return
}
response := dto.UserProfileResponse{
ID: profile.ID,
Email: profile.Email,
FirstName: profile.FirstName,
LastName: profile.LastName,
FullName: profile.FullName,
IsActive: profile.IsActive,
Role: profile.Role,
CreatedAt: profile.CreatedAt,
LastLoginAt: profile.LastLoginAt,
}
respondJSON(w, http.StatusOK, response)
}
// UpdateProfile handles updating user profile
func (h *UserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value("user_id").(string)
var req dto.UpdateProfileRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err)
return
}
ucReq := usecase.UpdateUserProfileRequest{
UserID: userID,
FirstName: req.FirstName,
LastName: req.LastName,
}
if err := h.profileUC.UpdateProfile(r.Context(), ucReq); err != nil {
respondError(w, http.StatusBadRequest, "Update failed", err)
return
}
respondJSON(w, http.StatusOK, map[string]string{"message": "Profile updated successfully"})
}
// Helper functions
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, status int, message string, err error) {
response := dto.ErrorResponse{
Error: message,
Message: err.Error(),
}
respondJSON(w, status, response)
}
internal/handler/http/middleware.go:
package http
import (
"context"
"myapp/internal/usecase"
"net/http"
"strings"
)
// AuthMiddleware creates an authentication middleware
func AuthMiddleware(tokenGen usecase.TokenGenerator) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
respondError(w, http.StatusUnauthorized, "Missing authorization header", nil)
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
respondError(w, http.StatusUnauthorized, "Invalid authorization header", nil)
return
}
token := parts[1]
userID, err := tokenGen.Validate(token)
if err != nil {
respondError(w, http.StatusUnauthorized, "Invalid token", err)
return
}
// Add user ID to context
ctx := context.WithValue(r.Context(), "user_id", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// CORSMiddleware adds CORS headers
func CORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
// LoggingMiddleware logs HTTP requests
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simple logging - in production, use a proper logger
println(r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
Main Application (Frameworks & Drivers)
cmd/api/main.go:
package main
import (
"database/sql"
"log"
"net/http"
"os"
"time"
"myapp/internal/handler/http"
"myapp/internal/repository/postgres"
"myapp/internal/security"
"myapp/internal/usecase"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
)
func main() {
// Load configuration
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
dbURL = "postgres://user:password@localhost:5432/userdb?sslmode=disable"
}
jwtSecret := os.Getenv("JWT_SECRET")
if jwtSecret == "" {
jwtSecret = "your-secret-key-change-in-production"
}
// Initialize database
db, err := sql.Open("postgres", dbURL)
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
log.Fatal("Failed to ping database:", err)
}
// Initialize repositories
userRepo := postgres.NewUserRepository(db)
roleRepo := postgres.NewRoleRepository(db)
// Initialize security services
hasher := security.NewBcryptHasher()
tokenGen := security.NewJWTGenerator(jwtSecret, 24*time.Hour)
idGen := security.NewUUIDGenerator()
// Initialize use cases
registerUC := usecase.NewUserRegistrationUseCase(userRepo, roleRepo, hasher, idGen)
loginUC := usecase.NewUserAuthenticationUseCase(userRepo, hasher, tokenGen)
profileUC := usecase.NewUserProfileUseCase(userRepo, roleRepo)
// Initialize handlers
userHandler := httpHandler.NewUserHandler(registerUC, loginUC, profileUC)
// Setup router
router := mux.NewRouter()
// Public routes
router.HandleFunc("/api/register", userHandler.Register).Methods("POST")
router.HandleFunc("/api/login", userHandler.Login).Methods("POST")
// Protected routes
protected := router.PathPrefix("/api").Subrouter()
protected.Use(httpHandler.AuthMiddleware(tokenGen))
protected.HandleFunc("/profile", userHandler.GetProfile).Methods("GET")
protected.HandleFunc("/profile", userHandler.UpdateProfile).Methods("PUT")
// Apply global middleware
router.Use(httpHandler.CORSMiddleware)
router.Use(httpHandler.LoggingMiddleware)
// Start server
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("Server starting on port %s", port)
if err := http.ListenAndServe(":"+port, router); err != nil {
log.Fatal("Server failed to start:", err)
}
}
Database Migrations
migrations/001_create_users_table.sql:
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(36) PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
role_id VARCHAR(36) NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
last_login_at TIMESTAMP,
INDEX idx_email (email),
INDEX idx_role_id (role_id)
);
migrations/002_create_roles_table.sql:
CREATE TABLE IF NOT EXISTS roles (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
permissions JSONB NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Insert default roles
INSERT INTO roles (id, name, description, permissions) VALUES
('admin', 'Administrator', 'Full system access', '["users:read", "users:write", "users:delete", "roles:manage"]'),
('user', 'User', 'Basic user access', '["users:read"]')
ON CONFLICT (id) DO NOTHING;
Testing Strategy {#testing}
Unit Testing
Domain layer tests (test only business logic):
package domain_test
import (
"myapp/internal/domain"
"testing"
)
func TestUser_Validate(t *testing.T) {
tests := []struct {
name string
user *domain.User
wantErr error
}{
{
name: "valid user",
user: &domain.User{
Email: "[email protected]",
FirstName: "John",
LastName: "Doe",
},
wantErr: nil,
},
{
name: "invalid email",
user: &domain.User{
Email: "invalid-email",
FirstName: "John",
LastName: "Doe",
},
wantErr: domain.ErrInvalidEmail,
},
{
name: "missing first name",
user: &domain.User{
Email: "[email protected]",
LastName: "Doe",
},
wantErr: errors.New("first name and last name are required"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.user.Validate()
if err != nil && tt.wantErr == nil {
t.Errorf("expected no error, got %v", err)
}
if err == nil && tt.wantErr != nil {
t.Errorf("expected error %v, got nil", tt.wantErr)
}
})
}
}
func TestUser_FullName(t *testing.T) {
user := &domain.User{
FirstName: "John",
LastName: "Doe",
}
expected := "John Doe"
if got := user.FullName(); got != expected {
t.Errorf("FullName() = %v, want %v", got, expected)
}
}
Use case tests with mocks:
package usecase_test
import (
"context"
"myapp/internal/domain"
"myapp/internal/usecase"
"testing"
)
// Mock repository
type mockUserRepository struct {
saveFn func(ctx context.Context, user *domain.User) error
findByEmailFn func(ctx context.Context, email string) (*domain.User, error)
}
func (m *mockUserRepository) Save(ctx context.Context, user *domain.User) error {
return m.saveFn(ctx, user)
}
func (m *mockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
return m.findByEmailFn(ctx, email)
}
func (m *mockUserRepository) FindByID(ctx context.Context, id string) (*domain.User, error) {
return nil, nil
}
func (m *mockUserRepository) Update(ctx context.Context, user *domain.User) error {
return nil
}
func (m *mockUserRepository) Delete(ctx context.Context, id string) error {
return nil
}
func (m *mockUserRepository) List(ctx context.Context, limit, offset int) ([]*domain.User, error) {
return nil, nil
}
// Mock role repository
type mockRoleRepository struct {
findByNameFn func(ctx context.Context, name string) (*domain.Role, error)
}
func (m *mockRoleRepository) FindByName(ctx context.Context, name string) (*domain.Role, error) {
return m.findByNameFn(ctx, name)
}
func (m *mockRoleRepository) FindByID(ctx context.Context, id string) (*domain.Role, error) {
return nil, nil
}
func (m *mockRoleRepository) List(ctx context.Context) ([]*domain.Role, error) {
return nil, nil
}
// Mock hasher
type mockHasher struct {
hashFn func(password string) (string, error)
}
func (m *mockHasher) Hash(password string) (string, error) {
return m.hashFn(password)
}
func (m *mockHasher) Compare(hashedPassword, password string) error {
return nil
}
// Mock ID generator
type mockIDGenerator struct{}
func (m *mockIDGenerator) Generate() string {
return "test-id-123"
}
func TestUserRegistrationUseCase_Execute(t *testing.T) {
ctx := context.Background()
t.Run("successful registration", func(t *testing.T) {
userRepo := &mockUserRepository{
findByEmailFn: func(ctx context.Context, email string) (*domain.User, error) {
return nil, domain.ErrUserNotFound
},
saveFn: func(ctx context.Context, user *domain.User) error {
return nil
},
}
roleRepo := &mockRoleRepository{
findByNameFn: func(ctx context.Context, name string) (*domain.Role, error) {
return domain.RoleUser, nil
},
}
hasher := &mockHasher{
hashFn: func(password string) (string, error) {
return "hashed_" + password, nil
},
}
idGen := &mockIDGenerator{}
uc := usecase.NewUserRegistrationUseCase(userRepo, roleRepo, hasher, idGen)
req := usecase.RegisterUserRequest{
Email: "[email protected]",
Password: "password123",
FirstName: "John",
LastName: "Doe",
}
result, err := uc.Execute(ctx, req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Email != req.Email {
t.Errorf("expected email %s, got %s", req.Email, result.Email)
}
})
t.Run("user already exists", func(t *testing.T) {
userRepo := &mockUserRepository{
findByEmailFn: func(ctx context.Context, email string) (*domain.User, error) {
return &domain.User{Email: email}, nil
},
}
roleRepo := &mockRoleRepository{}
hasher := &mockHasher{}
idGen := &mockIDGenerator{}
uc := usecase.NewUserRegistrationUseCase(userRepo, roleRepo, hasher, idGen)
req := usecase.RegisterUserRequest{
Email: "[email protected]",
Password: "password123",
FirstName: "John",
LastName: "Doe",
}
_, err := uc.Execute(ctx, req)
if err != domain.ErrUserAlreadyExists {
t.Errorf("expected ErrUserAlreadyExists, got %v", err)
}
})
}
Integration Testing
package integration_test
import (
"context"
"database/sql"
"myapp/internal/domain"
"myapp/internal/repository/postgres"
"testing"
_ "github.com/lib/pq"
)
func setupTestDB(t *testing.T) *sql.DB {
db, err := sql.Open("postgres", "postgres://test:test@localhost:5432/testdb?sslmode=disable")
if err != nil {
t.Fatal(err)
}
// Run migrations
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(36) PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
role_id VARCHAR(36) NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
last_login_at TIMESTAMP
)
`)
if err != nil {
t.Fatal(err)
}
return db
}
func teardownTestDB(t *testing.T, db *sql.DB) {
_, err := db.Exec("DROP TABLE IF EXISTS users")
if err != nil {
t.Fatal(err)
}
db.Close()
}
func TestPostgresUserRepository_Integration(t *testing.T) {
db := setupTestDB(t)
defer teardownTestDB(t, db)
repo := postgres.NewUserRepository(db)
ctx := context.Background()
t.Run("save and find user", func(t *testing.T) {
user := &domain.User{
ID: "test-123",
Email: "[email protected]",
PasswordHash: "hashed",
FirstName: "John",
LastName: "Doe",
RoleID: "user",
IsActive: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := repo.Save(ctx, user)
if err != nil {
t.Fatalf("failed to save user: %v", err)
}
found, err := repo.FindByEmail(ctx, user.Email)
if err != nil {
t.Fatalf("failed to find user: %v", err)
}
if found.Email != user.Email {
t.Errorf("expected email %s, got %s", user.Email, found.Email)
}
})
}
End-to-End Testing
package e2e_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"myapp/internal/handler/dto"
)
func TestUserRegistrationFlow(t *testing.T) {
// Setup test server
server := setupTestServer(t)
defer server.Close()
// Register user
regReq := dto.RegisterUserRequest{
Email: "[email protected]",
Password: "password123",
FirstName: "John",
LastName: "Doe",
}
regBody, _ := json.Marshal(regReq)
resp, err := http.Post(server.URL+"/api/register", "application/json", bytes.NewBuffer(regBody))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Errorf("expected status 201, got %d", resp.StatusCode)
}
var regResp dto.RegisterUserResponse
json.NewDecoder(resp.Body).Decode(®Resp)
// Login
loginReq := dto.LoginRequest{
Email: "[email protected]",
Password: "password123",
}
loginBody, _ := json.Marshal(loginReq)
resp2, err := http.Post(server.URL+"/api/login", "application/json", bytes.NewBuffer(loginBody))
if err != nil {
t.Fatal(err)
}
defer resp2.Body.Close()
if resp2.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp2.StatusCode)
}
var loginResp dto.LoginResponse
json.NewDecoder(resp2.Body).Decode(&loginResp)
if loginResp.Token == "" {
t.Error("expected token, got empty string")
}
// Get profile with token
req, _ := http.NewRequest("GET", server.URL+"/api/profile", nil)
req.Header.Set("Authorization", "Bearer "+loginResp.Token)
resp3, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp3.Body.Close()
if resp3.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp3.StatusCode)
}
}
Common Patterns and Practices {#patterns}
1. Repository Pattern
Already demonstrated above. Key points:
- Define interface in use case layer
- Implement in repository layer
- Use dependency injection
2. Factory Pattern
package factory
import (
"myapp/internal/domain"
"myapp/internal/security"
)
// UserFactory creates users with defaults
type UserFactory struct {
idGen security.IDGenerator
defaultRole string
}
func NewUserFactory(idGen security.IDGenerator) *UserFactory {
return &UserFactory{
idGen: idGen,
defaultRole: "user",
}
}
func (f *UserFactory) CreateUser(email, firstName, lastName string) (*domain.User, error) {
user, err := domain.NewUser(email, firstName, lastName)
if err != nil {
return nil, err
}
user.ID = f.idGen.Generate()
user.RoleID = f.defaultRole
user.IsActive = true
return user, nil
}
3. Specification Pattern
package specification
import "myapp/internal/domain"
// UserSpecification defines a specification for users
type UserSpecification interface {
IsSatisfiedBy(user *domain.User) bool
}
// ActiveUserSpec checks if user is active
type ActiveUserSpec struct{}
func (s *ActiveUserSpec) IsSatisfiedBy(user *domain.User) bool {
return user.IsActive
}
// EmailDomainSpec checks if user email has specific domain
type EmailDomainSpec struct {
domain string
}
func (s *EmailDomainSpec) IsSatisfiedBy(user *domain.User) bool {
return strings.HasSuffix(user.Email, "@"+s.domain)
}
// AndSpec combines multiple specifications
type AndSpec struct {
specs []UserSpecification
}
func (s *AndSpec) IsSatisfiedBy(user *domain.User) bool {
for _, spec := range s.specs {
if !spec.IsSatisfiedBy(user) {
return false
}
}
return true
}
4. Event-Driven Architecture
package events
import "myapp/internal/domain"
// Event represents a domain event
type Event interface {
EventName() string
}
// UserRegisteredEvent is emitted when a user registers
type UserRegisteredEvent struct {
User *domain.User
}
func (e *UserRegisteredEvent) EventName() string {
return "user.registered"
}
// EventPublisher publishes events
type EventPublisher interface {
Publish(event Event) error
}
// EventHandler handles events
type EventHandler interface {
Handle(event Event) error
}
// EventBus manages event publishing and handling
type EventBus struct {
handlers map[string][]EventHandler
}
func NewEventBus() *EventBus {
return &EventBus{
handlers: make(map[string][]EventHandler),
}
}
func (b *EventBus) Subscribe(eventName string, handler EventHandler) {
b.handlers[eventName] = append(b.handlers[eventName], handler)
}
func (b *EventBus) Publish(event Event) error {
handlers, ok := b.handlers[event.EventName()]
if !ok {
return nil
}
for _, handler := range handlers {
if err := handler.Handle(event); err != nil {
return err
}
}
return nil
}
5. CQRS (Command Query Responsibility Segregation)
// Command side
type CreateUserCommand struct {
Email string
Password string
FirstName string
LastName string
}
type CreateUserCommandHandler struct {
userRepo UserRepository
hasher PasswordHasher
}
func (h *CreateUserCommandHandler) Handle(ctx context.Context, cmd CreateUserCommand) error {
// Handle command
return nil
}
// Query side
type GetUserQuery struct {
UserID string
}
type GetUserQueryHandler struct {
userRepo UserRepository
}
func (h *GetUserQueryHandler) Handle(ctx context.Context, query GetUserQuery) (*domain.User, error) {
return h.userRepo.FindByID(ctx, query.UserID)
}
Pitfalls and How to Avoid Them {#pitfalls}
1. Leaking Domain Logic to Handlers
❌ Bad:
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
// Validation in handler
if len(req.Password) < 8 {
respondError(w, 400, "Password too short")
return
}
// Business logic in handler
user := &domain.User{
Email: req.Email,
Password: hashPassword(req.Password),
}
h.db.Save(user)
}
✅ Good:
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
// Handler only translates HTTP to domain
ucReq := usecase.RegisterUserRequest{
Email: req.Email,
Password: req.Password,
}
result, err := h.registerUC.Execute(r.Context(), ucReq)
// Handle result
}
2. Use Cases Depending on Frameworks
❌ Bad:
type CreateUserUseCase struct {
db *gorm.DB // Framework dependency
}
✅ Good:
type CreateUserUseCase struct {
repo UserRepository // Interface dependency
}
3. Fat Entities
❌ Bad:
type User struct {
// ... many fields
OrderHistory []Order
PaymentMethods []PaymentMethod
Preferences UserPreferences
// Becomes god object
}
func (u *User) PlaceOrder() { /* complex logic */ }
func (u *User) ProcessPayment() { /* complex logic */ }
✅ Good:
// Keep entities focused
type User struct {
ID string
Email string
// Only user-specific fields
}
// Separate concerns into different aggregates
type Order struct { /* ... */ }
type Payment struct { /* ... */ }
// Use services for cross-aggregate operations
type OrderService struct {
userRepo UserRepository
orderRepo OrderRepository
}
4. Anemic Domain Model
❌ Bad:
type User struct {
ID string
Email string
// No behavior, just data
}
// All logic in services
type UserService struct {
repo UserRepository
}
func (s *UserService) ValidateUser(user *User) error {
// Should be in domain
}
✅ Good:
type User struct {
ID string
Email string
}
// Behavior in domain
func (u *User) Validate() error {
// Business rules here
}
func (u *User) IsActive() bool {
// Business logic here
}
5. Circular Dependencies
❌ Bad:
domain → usecase → domain # Circular!
✅ Good:
handlers → usecase → domain
↓
repository
6. Testing Without Interfaces
❌ Bad:
type UserService struct {
repo *PostgresUserRepository // Concrete type
}
✅ Good:
type UserService struct {
repo UserRepository // Interface - easy to mock
}
Real-World Considerations {#real-world}
Performance Optimization
Caching layer:
type CachedUserRepository struct {
repo UserRepository
cache Cache
}
func (r *CachedUserRepository) FindByID(ctx context.Context, id string) (*domain.User, error) {
// Try cache first
if user, ok := r.cache.Get("user:" + id); ok {
return user.(*domain.User), nil
}
// Fall back to repository
user, err := r.repo.FindByID(ctx, id)
if err != nil {
return nil, err
}
// Cache result
r.cache.Set("user:"+id, user, 5*time.Minute)
return user, nil
}
Error Handling
Structured errors:
package errors
type AppError struct {
Code string
Message string
Err error
}
func (e *AppError) Error() string {
if e.Err != nil {
return e.Message + ": " + e.Err.Error()
}
return e.Message
}
func NewValidationError(msg string) *AppError {
return &AppError{Code: "VALIDATION_ERROR", Message: msg}
}
func NewNotFoundError(msg string) *AppError {
return &AppError{Code: "NOT_FOUND", Message: msg}
}
Logging and Observability
type LoggingUserRepository struct {
repo UserRepository
logger Logger
}
func (r *LoggingUserRepository) Save(ctx context.Context, user *domain.User) error {
r.logger.Info("Saving user", "email", user.Email)
start := time.Now()
err := r.repo.Save(ctx, user)
duration := time.Since(start)
if err != nil {
r.logger.Error("Failed to save user", "error", err, "duration", duration)
return err
}
r.logger.Info("User saved successfully", "duration", duration)
return nil
}
Configuration Management
package config
import "os"
type Config struct {
Database DatabaseConfig
Server ServerConfig
JWT JWTConfig
}
type DatabaseConfig struct {
URL string
MaxConnections int
ConnMaxLifetime time.Duration
}
type ServerConfig struct {
Port string
ReadTimeout time.Duration
WriteTimeout time.Duration
}
type JWTConfig struct {
Secret string
Duration time.Duration
}
func Load() (*Config, error) {
return &Config{
Database: DatabaseConfig{
URL: os.Getenv("DATABASE_URL"),
MaxConnections: 25,
ConnMaxLifetime: 5 * time.Minute,
},
Server: ServerConfig{
Port: os.Getenv("PORT"),
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
},
JWT: JWTConfig{
Secret: os.Getenv("JWT_SECRET"),
Duration: 24 * time.Hour,
},
}, nil
}
Graceful Shutdown
func main() {
// ... setup code ...
server := &http.Server{
Addr: ":8080",
Handler: router,
}
// Start server in goroutine
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exited")
}
Conclusion
Clean Architecture in Go provides:
- Testability: Every layer can be tested independently
- Maintainability: Changes are isolated to specific layers
- Flexibility: Easy to swap implementations
- Scalability: Clear boundaries support team growth
- Longevity: Business logic survives framework changes
Key Takeaways:
- Dependencies point inward (Dependency Rule)
- Entities contain business logic, not frameworks
- Use cases orchestrate business workflows
- Interfaces define contracts, implementations are details
- Keep frameworks and databases at the edges
Remember: Clean Architecture is about creating sustainable systems. The initial overhead pays dividends as your project grows and evolves.
Further Reading:
- Clean Architecture by Robert C. Martin
- Domain-Driven Design by Eric Evans
- Implementing Domain-Driven Design by Vaughn Vernon
- Go best practices: https://golang.org/doc/effective_go
Get the Complete Guide: The Complete Guide to Clean Architecture
Prefer to read offline? Get the complete PDF, ePub, and source code bundle.
Buy Now for $19Includes PDF, ePub, and full source code bundle. Payments securely processed via Gumroad.