This is an old revision of the document!
Table of Contents
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
