# Clean Architecture for Go DDD Projects ## Directory Structure ``` project-root/ ├── cmd/ │ └── api/ │ └── main.go # Application entry point ├── internal/ │ ├── domain/ # CORE - No external dependencies │ │ └── / │ │ ├── aggregate.go # Aggregate roots │ │ ├── entity.go # Child entities │ │ ├── value_objects.go # Value objects │ │ ├── repository.go # Repository INTERFACES │ │ ├── service.go # Domain services │ │ ├── events.go # Domain events │ │ └── errors.go # Domain-specific errors │ │ │ ├── application/ # USE CASES - Depends on Domain │ │ └── / │ │ ├── service.go # Application services │ │ ├── commands.go # Command handlers (CQRS) │ │ ├── queries.go # Query handlers (CQRS) │ │ └── dto.go # Data Transfer Objects │ │ │ └── infrastructure/ # ADAPTERS - Depends on Domain & App │ ├── persistence/ │ │ └── / │ │ ├── postgres_repository.go │ │ ├── memory_repository.go │ │ └── models.go # DB models (separate from domain) │ ├── messaging/ │ │ └── kafka_publisher.go │ └── http/ │ ├── handlers/ │ │ └── _handler.go │ ├── middleware/ │ └── router.go ├── pkg/ # Shared utilities (if needed) │ └── errors/ │ └── errors.go └── go.mod ``` ## Layer Responsibilities ### Domain Layer (`internal/domain/`) **Purpose**: Core business logic, independent of all frameworks and infrastructure. **Contains**: - Aggregates, Entities, Value Objects - Repository interfaces (not implementations!) - Domain Services - Domain Events - Domain Errors **Rules**: - NO imports from `application/` or `infrastructure/` - NO database packages, HTTP packages, or framework code - NO struct tags (`json:`, `gorm:`, etc.) - Pure Go, pure business logic **Example**: ```go package accounts // Domain layer - NO external imports type Account struct { id AccountID balance Money status Status } // Repository interface - implementation is in infrastructure type AccountRepository interface { Save(ctx context.Context, account *Account) error FindByID(ctx context.Context, id AccountID) (*Account, error) } ``` ### Application Layer (`internal/application/`) **Purpose**: Orchestrate use cases, coordinate domain objects. **Contains**: - Application Services (Use Cases) - Command/Query Handlers (CQRS) - DTOs for input/output - Transaction management **Rules**: - Imports from `domain/` only - NO direct database access - NO HTTP/transport concerns - Coordinates domain objects, doesn't contain business logic **Example**: ```go package accounts import ( "context" "myapp/internal/domain/accounts" ) type TransferService struct { accountRepo accounts.AccountRepository txManager TransactionManager } func (s *TransferService) Transfer(ctx context.Context, cmd TransferCommand) error { return s.txManager.Execute(ctx, func(ctx context.Context) error { from, err := s.accountRepo.FindByID(ctx, cmd.FromAccountID) if err != nil { return err } to, err := s.accountRepo.FindByID(ctx, cmd.ToAccountID) if err != nil { return err } // Domain logic in domain objects if err := from.Withdraw(cmd.Amount); err != nil { return err } if err := to.Deposit(cmd.Amount); err != nil { return err } // Coordinate persistence if err := s.accountRepo.Save(ctx, from); err != nil { return err } return s.accountRepo.Save(ctx, to) }) } ``` ### Infrastructure Layer (`internal/infrastructure/`) **Purpose**: Implement interfaces defined in domain, connect to external systems. **Contains**: - Repository implementations (Postgres, Redis, etc.) - Message queue publishers/consumers - External API clients - HTTP handlers - Database models (separate from domain entities) **Rules**: - Implements interfaces from `domain/` - Can import from `domain/` and `application/` - Contains all framework-specific code - Handles mapping between domain and persistence models **Example**: ```go package persistence import ( "context" "database/sql" "myapp/internal/domain/accounts" ) // Compile-time check var _ accounts.AccountRepository = (*PostgresAccountRepository)(nil) type PostgresAccountRepository struct { db *sql.DB } func (r *PostgresAccountRepository) Save(ctx context.Context, account *accounts.Account) error { // Map domain to DB model model := toDBModel(account) // Persist _, err := r.db.ExecContext(ctx, "INSERT INTO accounts ...", ...) return err } func (r *PostgresAccountRepository) FindByID(ctx context.Context, id accounts.AccountID) (*accounts.Account, error) { var model accountDBModel err := r.db.QueryRowContext(ctx, "SELECT ... WHERE id = $1", id.String()).Scan(...) if err != nil { return nil, err } // Map DB model to domain return toDomain(model), nil } ``` ## Dependency Injection Wire dependencies at application startup: ```go // cmd/api/main.go func main() { // Infrastructure db := connectDB() accountRepo := persistence.NewPostgresAccountRepository(db) // Application transferService := application.NewTransferService(accountRepo) // HTTP handler := http.NewAccountHandler(transferService) // Start server router := setupRouter(handler) http.ListenAndServe(":8080", router) } ``` ## Package Naming Conventions | Layer | Package Name | Example | |-------|--------------|---------| | Domain | Bounded context name | `accounts`, `transfers`, `loyalty` | | Application | Same as domain | `accounts` (in `application/accounts/`) | | Infrastructure | Descriptive | `persistence`, `messaging`, `http` | ## Import Rules ```go // ✅ ALLOWED domain/accounts -> (nothing external) application/accounts -> domain/accounts infrastructure/persistence -> domain/accounts, application/accounts // ❌ NOT ALLOWED domain/accounts -> application/accounts // Domain can't know application domain/accounts -> infrastructure/ // Domain can't know infra application/accounts -> infrastructure/ // App can't know infra ``` ## Testing Strategy | Layer | Test Type | Mocking | |-------|-----------|---------| | Domain | Unit tests | No mocks needed | | Application | Unit tests | Mock repositories | | Infrastructure | Integration tests | Real DB (testcontainers) | | E2E | API tests | Full stack | ```go // Domain test - no mocks func TestAccount_Withdraw(t *testing.T) { account := accounts.NewAccount(id, accounts.MustMoney(100)) err := account.Withdraw(accounts.MustMoney(50)) assert.NoError(t, err) assert.Equal(t, accounts.MustMoney(50), account.Balance()) } // Application test - mock repository func TestTransferService(t *testing.T) { repo := &MockAccountRepository{} service := NewTransferService(repo) // ... } ```