Master DDD with Go: Build Clean, Scalable Backends Using Hexagonal Architecture
This guide explains why combining Domain‑Driven Design with Go, outlines core DDD concepts such as hexagonal architecture, entities, value objects, aggregates, services, repositories, and application services, and provides a complete project structure and runnable code examples.
Why Choose DDD + Go
In complex business scenarios, traditional three‑layer architectures quickly become tangled "spaghetti code". DDD concentrates logic in the domain model, decouples business from technical details via hexagonal architecture, and enforces a ubiquitous language that aligns requirements, design, and code.
Focus on core business : logic lives in the domain model instead of controllers or persistence code.
Technical‑business decoupling : ports and adapters isolate the core from databases, message queues, etc.
Unified team language : a shared ubiquitous language ensures consistent understanding across the team.
Go’s simple syntax, interface‑first design, and composition features make it a natural fit for DDD patterns.
Core Concepts
1. Hexagonal Architecture
The business logic sits at the system’s center, exposing Ports that define interaction contracts and Adapters that implement those contracts to connect external systems. Changing a database or message queue does not affect the core.
[Adapters] ←——→ [Ports] ←——→ [Domain Core]2. Subdomains and Bounded Contexts
Subdomain : split the system by business functionality, e.g., an e‑commerce system may have an "order" subdomain and an "inventory" subdomain.
Bounded Context : each subdomain is modeled independently; different contexts interact through explicit interfaces, preventing model confusion.
3. Entity
Has a unique identifier (ID).
Lifecycle can change.
Encapsulates business rules and behavior.
Examples: Order, User.
4. Value Object
No unique identifier.
Immutable.
Represents conceptual business attributes.
Examples: Money, Address.
5. Aggregate
An aggregate is a cluster of objects that must stay consistent, managed by a single Aggregate Root . All business operations go through the root.
Example: An Order aggregate contains the Order entity and OrderItem value objects; modifications must be performed via the Order entity.
6. Domain Service
When business logic spans multiple entities or aggregates, a domain service coordinates the work, e.g., an "order payment" service that orchestrates both the order and payment aggregates.
7. Repository
Repositories encapsulate persistence logic:
The domain layer depends only on repository interfaces.
The infrastructure layer provides concrete implementations, adhering to the Dependency Inversion Principle.
8. Application Service
Application services act as the entry point for use‑cases:
Orchestrate domain models, repositories, and external services.
Handle transactions.
Expose a unified interface to the outside world.
Project Structure Template
project/
├── cmd/ # program entry
│ └── main.go
├── internal/
│ ├── order/
│ │ ├── domain/
│ │ │ ├── entity/
│ │ │ │ └── order.go
│ │ │ ├── vo/
│ │ │ │ └── money.go
│ │ │ ├── service/
│ │ │ │ └── payment_service.go
│ │ │ └── repository.go
│ │ ├── app/
│ │ │ └── order_service.go
│ │ └── infra/
│ │ └── repo_mysql.go
│ └── shared/
├── pkg/ # reusable utilitiesCode Examples
Entity
package entity
type Order struct {
ID string
Items []OrderItem
Status string
}
type OrderItem struct {
ProductID string
Quantity int
}
func (o *Order) AddItem(productID string, qty int) {
o.Items = append(o.Items, OrderItem{ProductID: productID, Quantity: qty})
}
func (o *Order) MarkPaid() {
o.Status = "PAID"
}Value Object
package vo
type Money struct {
Amount int64
Currency string
}Repository Interface
package domain
import "project/internal/order/domain/entity"
type OrderRepository interface {
Save(order *entity.Order) error
FindByID(id string) (*entity.Order, error)
}Application Service
package app
import (
"project/internal/order/domain"
"project/internal/order/domain/entity"
)
type OrderService struct {
repo domain.OrderRepository
}
func NewOrderService(r domain.OrderRepository) *OrderService {
return &OrderService{repo: r}
}
func (s *OrderService) CreateOrder(items []entity.OrderItem) (*entity.Order, error) {
order := &entity.Order{ID: "123", Items: items, Status: "CREATED"}
if err := s.repo.Save(order); err != nil {
return nil, err
}
return order, nil
}Repository Implementation
package infra
import (
"project/internal/order/domain/entity"
"project/internal/order/domain"
)
type MysqlOrderRepo struct{}
func (r *MysqlOrderRepo) Save(o *entity.Order) error {
// TODO: insert into DB
return nil
}
func (r *MysqlOrderRepo) FindByID(id string) (*entity.Order, error) {
// TODO: query DB
return &entity.Order{ID: id, Status: "CREATED"}, nil
}Advantages Recap
High cohesion, low coupling : business logic stays in the domain layer, isolated from technical details.
Easy extensibility : adapters can be swapped (e.g., DB, message queue) without touching core code.
Testability : interfaces enable mock injection for unit tests.
Unified language : the ubiquitous language reduces communication overhead across teams.
Visualization
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Wrench
Focuses on code debugging, performance optimization, and real-world engineering, sharing efficient development tips and pitfall guides. We break down technical challenges in a down-to-earth style, helping you craft handy tools so every line of code becomes a problem‑solving weapon. 🔧💻
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
