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.

Code Wrench
Code Wrench
Code Wrench
Master DDD with Go: Build Clean, Scalable Backends Using Hexagonal Architecture

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 utilities

Code 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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GoDDDDomain Modeling
Code Wrench
Written by

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. 🔧💻

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.