# DDD and Clean Architecture in Go: A Pragmatic Approach Without Overengineering
Technical debt stemming from architectural issues consumes up to 40% of companies' IT budgets. In Go projects, this often manifests as overly complex structures: 200 files filled with empty interfaces and layers where business logic gets buried. The root cause is misapplying DDD and Clean Architecture patterns borrowed from Java or C#. But when used pragmatically, these patterns solve complexity issues. Here's how to avoid overengineering and build a maintainable system.
Why Do Go Projects Turn into a Maze of Files?
Developers coming to Go from other languages often carry over architectural habits that don't fit idiomatic Go. According to the Go Developer Survey 2024, the top issue is maintaining consistent code standards due to varying experience levels and non-idiomatic patterns. For example, creating folder structures like domain, application, and infrastructure mimicking Java, without grasping the core principles.
Carnegie Mellon research confirms: architectural problems are the primary source of technical debt. In Go, it looks like this:
- A
dtofolder with duplicate structs. mappersbetween layers for trivial conversions.- Interfaces in
domainthat depend on infrastructure (e.g.,database/sql). - Use cases that just call repositories without any business logic.
The result: 200 files, but none describing actual business rules. Clean Architecture and DDD become not the solution, but the problem itself.
DDD: Not About Folders, But Language and Boundaries
Domain-Driven Design is not about a set of folders—it's an approach to modeling the problem domain. Key concepts:
Ubiquitous Language
Developers and business stakeholders must use the same terms everywhere: in code, documentation, and discussions. If a manager says "confirm order," the code should have a Confirm() method, not SetStatus(). This eliminates confusion and speeds up onboarding.
Bounded Context
A large system consists of multiple contexts. For example, "customer" in sales (a lead with a funnel) and in support (tickets) are different models. Bounded Context defines clear boundaries where the model remains consistent. In microservices, one service typically aligns with one context.
Entity and Value Object
- Entity has a unique ID and maintains identity (e.g.,
Order). It contains business rules. - Value Object is immutable and defined by its attributes (e.g.,
Money). Two objects with identical fields are equivalent.
Example Value Object in Go:
type Money struct {
Amount int64
Currency string
}
func (m Money) Add(other Money) (Money, error) {
if m.Currency != other.Currency {
return Money{}, errors.New("currency mismatch")
}
return Money{Amount: m.Amount + other.Amount, Currency: m.Currency}, nil
}
Aggregate
A group of objects treated as a single unit. It has a root (Aggregate Root) through which all access occurs. For example, Order (root) includes OrderItem. Rule: changes are made only through the root, ensuring consistency.
Clean Architecture: Dependency Rule as the Foundation
The essence of Clean Architecture is that business logic shouldn't depend on implementation details (databases, frameworks). The single rule: dependencies point inward (Dependency Rule).
How It Works in Go
- domain: Contains the business model (Entity, Value Object, repository interfaces). No external imports except stdlib.
- application: Use cases that orchestrate the domain. Depends only on domain.
- infrastructure: Adapter implementations (databases, HTTP clients). Depends on domain via interfaces.
- delivery: Entry points (HTTP, gRPC). Depends on application.
If domain imports database/sql, the architecture is broken. Correct example: repository interface declared in domain, implementation in infrastructure/postgres.
Benefits of Combining DDD and Clean Architecture
| Aspect | Effect | Improvement Metric |
|---------------|---------------------------------|--------------------|
| Testability | Domain isolation from infrastructure | +40% coverage |
| Flexibility | Swap adapters in hours | -90% time |
| Understandability | Clear component boundaries | -70% onboarding |
Real Project Structure: Avoiding 200 Files
Optimal structure for a Go service:
internal/
domain/
order.go # Aggregate Order
order_repo.go # Repository interface
application/
create_order.go # Use case
infrastructure/
postgres/
order_repo.go # Implementation
delivery/
http/
order_handler.go
Key principles:
- Repository interfaces in
domain, notinfrastructure. - No
dtoormappersfolders unless truly needed: reuse the same structs across layers if it doesn't violate boundaries. - Business logic only in
domain. For example,Cancel()method forOrderchecks status:
func (o *Order) Cancel() error {
if o.status == StatusShipped {
return errors.New("cannot cancel shipped order")
}
o.status = StatusCancelled
return nil
}
Key Takeaways
- Ubiquitous Language is the foundation of DDD. If code terms don't match business lingo, the model will be wrong.
- Dependency Rule is the core of Clean Architecture. Violations lead to spaghetti code.
- Pragmatism—don't create layers without need. In Go, you can often skip
dtoif structs align. - Aggregates define transaction boundaries. Change only through the root for consistency.
- Testing—domain tests run without infrastructure, speeding up development.
— Editorial Team
No comments yet.