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
<pre> 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) </pre>
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 | pointadd_repository.go | Domain (interfaces) |
Dependency Direction:
<pre> Infrastructure → Domain ← Application ← Presentation </pre>
III. Processing Flow (Request → Response)
<pre> 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 </pre>
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 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) 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 requests
Validate input
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 DB or external services
Infrastructure depends on Domain
V. 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 implementations
Enables easy mocking
mockRepo := &MockAddPointRepository{}
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 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:
<pre> Handler ↓ Usecase ↓ Repository Interface ↓ Repository Implementation ↓ Database </pre>
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, and scale
