User Tools

Site Tools


go:architecture:clean_arch

This is an old revision of the document!


A Guide to Using Clean Architecture with DDD (Domain-Driven Design) in Go

I. Introduction to Clean Architecture

Clean Architecture is a software architecture proposed by Robert C. Martin. Its goal is to clearly separate layers so the system is easier to maintain, extend, and test.

The pascalia-spec project applies Clean Architecture with 4 main layers:

Domain Layer: Core business logic (entities, interfaces, business rules)

Application / Usecase Layer: Application logic (orchestration, use cases)

Presentation Layer: User interface communication (handlers, APIs)

Infrastructure Layer: External technical concerns (DB, external APIs)

Key Principle:

Dependencies always point inward (outer layers depend on inner layers, never the opposite).

II. Folder Structure and Dependencies

Overall Structure

 internal/ ├── burton/ # Bounded context for "burton" domain │ ├── domain/ # Domain Layer │ │ └── repository/ # Interfaces (e.g., IAddPointRepository) │ ├── infrastructure/ # Infrastructure Layer │ │ └── repository/ # Repository implementations (DB access) │ └── usecase/ # Application / Usecase Layer ├── presentation/ # Presentation Layer │ └── grpc/ │ └── handler/ # gRPC handlers ├── constant/ # Constants (cross-cutting) ├── infrastructure/ # Shared infrastructure (DB transactions) └── validator/ # Input validation (cross-cutting) 

Roles and Dependencies

Layer Role Example Depends On
Domain Core business logic IAddPointRepository, business rules Nothing
Application / Usecase Application logic PointAddUsecase Domain
Presentation User communication SSOHandler Application
Infrastructure External technical concerns pointadd_repository.go Domain (interfaces)

Dependency Direction:

Infrastructure → Domain ← Application ← Presentation

III. Processing Flow (Request → Response)

 Client (gRPC Request) ↓ Presentation Layer (SSOHandler) - Receive request - Validate input ↓ Application Layer (ISSOUsecase) - Orchestrate business logic ↓ Domain Layer (Repository Interface) - Define contracts ↓ Infrastructure Layer (Repository Implementation) - Access DB / External API ↓ Database / External Service 

IV. Detailed Examples from Source Code

1. Domain Layer: Define Interfaces

 // internal/burton/domain/repository/add_point_repository.go type IAddPointRepository interface { FetchProcessingOrders( ctx context.Context, limit, offset int, ) ([]ProcessingOrders, error)
SaveOrders(
    ctx context.Context,
    orders []ValidOrder,
) error
 
 
}

Purpose:

Define business contracts

No implementation

No dependency on frameworks or DB

2. Application / Usecase Layer: Orchestration Logic

 // internal/burton/usecase/point_add_usecase.go type PointAddUsecase struct { repo IAddPointRepository }
 
func (u *PointAddUsecase) PointAdd(ctx context.Context) error {
orders, err := u.repo.FetchProcessingOrders(ctx, 200, 0)
if err != nil {
return err
}
 
// Business rules:
// - Calculate points:
//   product_total - adjusted_merchandize_total_tax
// - Generate CSV
// - Upload to Yappli
 
return nil
 
 
}

Purpose:

Orchestrate business logic

Depends only on Domain interfaces

Easy to unit test using mocks

3. Presentation Layer: gRPC Handler

 // internal/presentation/grpc/handler/burton/SSO.go type SSOHandler struct { SSOService usecase.ISSOUsecase }
 
func (h *SSOHandler) Authenticate(
ctx context.Context,
req *pb.AuthRequest,
) (*pb.AuthResponse, error) {
 
// Validate input
if err := validator.ValidateSSO(req); err != nil {
    return nil, err
}
 
// Call usecase
return h.SSOService.Authenticate(ctx, req)
 
 
}

Purpose:

Receive and validate requests

Delegate logic to Usecase

No business rules here

4. Infrastructure Layer: Repository Implementation

 // internal/burton/infrastructure/repository/pointadd_repository.go type AddPointRepository struct { db *sqlx.DB }
 
func (r *AddPointRepository) FetchProcessingOrders(
ctx context.Context,
limit, offset int,
) ([]ProcessingOrders, error) {
 
query := `
    SELECT ...
    FROM customers c
    INNER JOIN orders o ...
    LIMIT ? OFFSET ?
`
return r.db.SelectContext(ctx, &orders, query, limit, offset)
 
 
}

Purpose:

Implement domain interfaces

Access database or external services

Infrastructure depends on Domain, not vice versa

V. Best Practices When Coding

1. Dependencies Point Inward

Domain → depends on nothing

Application → depends only on Domain

Presentation → depends only on Application

Infrastructure → depends on Domain (via interfaces)

2. Use Interfaces and Dependency Injection

Inject interfaces, not concrete structs

Example:

mockRepo := &MockAddPointRepository{}

Enables easy unit testing and mocking

3. Separate Concerns

Constants → internal/constant/

Validation → internal/validator/

Database & Transactions → internal/infrastructure/database/

4. Testing Strategy

Unit Tests

Test each layer independently

Mock dependencies

Integration Tests

Use real DB and services

5. Error Handling

Use fastmedia/errors for structured errors

Log errors using slog in:

Presentation layer

Usecase layer

VI. Example: Implementing a New Feature

Feature: Register Customer

Steps:

Domain

Create ICustomerRepository

Application

Create RegisterCustomerUsecase

Presentation

Create gRPC handler RegisterHandler

Infrastructure

Implement CustomerRepository using sqlx

Flow:

Handler → Usecase → Repository Interface → Repository Implementation → DB

VII. Conclusion

By applying Clean Architecture in this project:

Dependencies always point inward

Interfaces decouple business logic from infrastructure

Each bounded context (e.g. burton) is clearly separated

Code becomes easier to:

Maintain

Test

Scale

go/architecture/clean_arch.1769735285.txt.gz · Last modified: by phong2018