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

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 contracts for business logic

No implementation

No dependency on DB or frameworks

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) // Batch size 200
if err != nil {
return err
}
// Calculate points:
// product_total - adjusted_merchandize_total_tax
// Generate CSV, upload to Yappli
return nil
}

Purpose:

Orchestrate business logic

Use interfaces, not implementations

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 requests

Validate input

Delegate logic to usecase

No business rules here

4. Infrastructure Layer: 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

Handle DB / external APIs

Depends on Domain, not Usecase or Presentation

IV. Best Practices When Coding

1. Dependencies Point Inward

Domain → depends on nothing

Application → depends on Domain

Presentation → depends on Application

Infrastructure → depends on Domain (via interfaces)

2. Use Interfaces and Dependency Injection

Inject interfaces, not concrete structs Example: usecase.ISSOUsecase

Easy to mock for testing:

mockRepo := &MockAddPointRepository{}

3. Separate Concerns

Constants → internal/constant/ Example: DefaultChunk = 200

Validation → internal/validator/

Transactions / DB setup → internal/infrastructure/database/

4. Testing Strategy

Unit Tests

Test each layer independently

Mock dependencies

Integration Tests

End-to-end with real DB

Example:

Test usecase with a mock repository

5. Error Handling

Use fastmedia/errors for structured errors

Log with slog in:

Presentation layer

Usecase layer

V. Example: Implementing a New Feature

Feature: Register Customer

Steps:

Domain

Create ICustomerRepository interface

Application

Create RegisterCustomerUsecase

Presentation

Create gRPC handler RegisterHandler

Infrastructure

Implement CustomerRepository using sqlx

Flow:

Handler → Usecase → Repository Interface → Repository Implementation → DB

VI. Conclusion

By following Clean Architecture in this project:

Dependencies always point inward

Interfaces decouple layers

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

Code becomes easier to:

Maintain

Test

Scale

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