Code2b DocsLocal preview

Go: DDD + Hexagonal Architecture

A practical Go architecture that combines Domain-Driven Design (DDD) with Hexagonal Architecture, while keeping a clean path toward microservices.

Folder-separated where it improves ownership, but not split so far that every file gets its own directory.


Project layout

project/
│
ā”œā”€ā”€ cmd/
│   └── api/
│       └── main.go
│
ā”œā”€ā”€ internal/
│   │
│   ā”œā”€ā”€ inbound/
│   │   │
│   │   ā”œā”€ā”€ http/
│   │   │   ā”œā”€ā”€ router.go
│   │   │   │
│   │   │   ā”œā”€ā”€ middleware/
│   │   │   │   ā”œā”€ā”€ auth.go
│   │   │   │   ā”œā”€ā”€ cors.go
│   │   │   │   ā”œā”€ā”€ logging.go
│   │   │   │   ā”œā”€ā”€ recovery.go
│   │   │   │   └── request_id.go
│   │   │   │
│   │   │   └── handler/
│   │   │       ā”œā”€ā”€ user_handler.go
│   │   │       ā”œā”€ā”€ product_handler.go
│   │   │       └── order_handler.go
│   │   │
│   │   └── grpc/
│   │       ā”œā”€ā”€ server.go
│   │       │
│   │       ā”œā”€ā”€ middleware/
│   │       │   ā”œā”€ā”€ auth.go
│   │       │   ā”œā”€ā”€ logging.go
│   │       │   └── recovery.go
│   │       │
│   │       └── handler/
│   │           ā”œā”€ā”€ user_handler.go
│   │           ā”œā”€ā”€ product_handler.go
│   │           └── order_handler.go
│   │
│   ā”œā”€ā”€ domain/
│   │   │
│   │   ā”œā”€ā”€ user/
│   │   │   ā”œā”€ā”€ model.go
│   │   │   ā”œā”€ā”€ repository.go
│   │   │   └── service.go
│   │   │
│   │   ā”œā”€ā”€ product/
│   │   │   ā”œā”€ā”€ model.go
│   │   │   ā”œā”€ā”€ repository.go
│   │   │   └── service.go
│   │   │
│   │   └── order/
│   │       ā”œā”€ā”€ model.go
│   │       ā”œā”€ā”€ repository.go
│   │       └── service.go
│   │
│   └── outbound/
│       │
│       ā”œā”€ā”€ db/
│       │   └── postgres.go
│       │
│       ā”œā”€ā”€ cache/
│       │   └── redis.go
│       │
│       ā”œā”€ā”€ repository/
│       │   ā”œā”€ā”€ user_repository.go
│       │   ā”œā”€ā”€ product_repository.go
│       │   └── order_repository.go
│       │
│       └── external/
│           ā”œā”€ā”€ payment.go
│           └── email.go
│
ā”œā”€ā”€ migrations/
│   ā”œā”€ā”€ 001_create_users.sql
│   ā”œā”€ā”€ 002_create_products.sql
│   └── 003_create_orders.sql
│
ā”œā”€ā”€ config/
│   └── config.go
│
ā”œā”€ā”€ go.mod
└── go.sum

Architecture diagram

                    ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
                    │       INBOUND        │
                    │                      │
                    │ HTTP / gRPC          │
                    │ Handlers             │
                    │ Middleware           │
                    ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
                               │
                               ā–¼
                    ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
                    │        DOMAIN        │
                    │                      │
                    │ user                 │
                    │ product              │
                    │ order                │
                    │                      │
                    │ Models               │
                    │ Services             │
                    │ Repository Ports     │
                    ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
                               │
                               ā–¼
                    ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
                    │       OUTBOUND       │
                    │                      │
                    │ PostgreSQL           │
                    │ Redis                │
                    │ External APIs        │
                    │ Repository Adapters  │
                    ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Layer responsibilities

inbound/
    How requests enter the application.
 
domain/
    What the business does.
 
outbound/
    How the application talks to external systems.

The domain defines the port:

domain/user/repository.go

Outbound provides the adapter:

outbound/repository/user_repository.go
User Repository Interface
          │
          │ port
          ā–¼
PostgreSQL Repository
          │
          │ adapter
          ā–¼
      PostgreSQL

The domain does not depend on HTTP, PostgreSQL, Redis, or any external provider. Dependency direction always points inward.


What goes in each file

cmd/api/main.go

Wire dependencies only: load config, open DB, construct repositories, services, handlers, start the server. No business logic.

internal/inbound/

Path Responsibility
http/router.go Route registration, global middleware
http/middleware/ Cross-cutting HTTP concerns (auth, logging, recovery)
http/handler/ Parse request, call domain service, write response
grpc/ Same split for gRPC if the service exposes both

Handlers are thin. They validate input shape, call the domain service, map errors to status codes.

internal/domain/{entity}/

File Responsibility
model.go Entity structs, value objects, domain errors, status constants
repository.go Interface (port) for persistence. No implementation here
service.go Business rules, orchestration, transactions at domain level
// domain/user/repository.go
type Repository interface {
    GetByID(ctx context.Context, id uuid.UUID) (*User, error)
    Create(ctx context.Context, user *User) error
}
 
// domain/user/service.go
type Service struct {
    repo Repository
}
 
func (s *Service) Register(ctx context.Context, email string) (*User, error) {
    // business rules live here
}

internal/outbound/

Path Responsibility
db/postgres.go Connection pool, health check
cache/redis.go Cache client setup
repository/ Implements domain repository interfaces
external/ Third-party API clients (Stripe, email, etc.)
// outbound/repository/user_repository.go
type PostgresUserRepository struct {
    db *sql.DB
}
 
func (r *PostgresUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*user.User, error) {
    // SQL here; maps rows to domain/user.User
}

config/

Environment loading, validation, defaults. Fail fast on missing required vars at startup.

migrations/

Numbered SQL files. See Naming Conventions for migration rules.


Dependency rules

  1. Domain imports nothing from inbound/ or outbound/.
  2. Inbound imports domain only (plus framework: Fiber, gRPC).
  3. Outbound imports domain to implement interfaces and return domain types.
  4. main imports everything and passes concrete adapters into constructors.
inbound  ──►  domain  ◄──  outbound
                  ā–²
                  │
                 cmd

Microservice extraction

Because each domain is already isolated, a domain can later become an independent service.

Extracting user:

user-service/
│
ā”œā”€ā”€ cmd/
│   └── api/
│       └── main.go
│
ā”œā”€ā”€ internal/
│   │
│   ā”œā”€ā”€ inbound/
│   │   ā”œā”€ā”€ http/
│   │   │   ā”œā”€ā”€ router.go
│   │   │   ā”œā”€ā”€ middleware/
│   │   │   │   ā”œā”€ā”€ auth.go
│   │   │   │   ā”œā”€ā”€ cors.go
│   │   │   │   ā”œā”€ā”€ logging.go
│   │   │   │   ā”œā”€ā”€ recovery.go
│   │   │   │   └── request_id.go
│   │   │   └── handler/
│   │   │       └── user_handler.go
│   │   │
│   │   └── grpc/
│   │       ā”œā”€ā”€ server.go
│   │       ā”œā”€ā”€ middleware/
│   │       │   ā”œā”€ā”€ auth.go
│   │       │   ā”œā”€ā”€ logging.go
│   │       │   └── recovery.go
│   │       └── handler/
│   │           └── user_handler.go
│   │
│   ā”œā”€ā”€ domain/
│   │   └── user/
│   │       ā”œā”€ā”€ model.go
│   │       ā”œā”€ā”€ repository.go
│   │       └── service.go
│   │
│   └── outbound/
│       ā”œā”€ā”€ db/
│       │   └── postgres.go
│       ā”œā”€ā”€ cache/
│       │   └── redis.go
│       ā”œā”€ā”€ repository/
│       │   └── user_repository.go
│       └── external/
│           └── email.go
│
ā”œā”€ā”€ migrations/
│   └── 001_create_users.sql
│
ā”œā”€ā”€ config/
│   └── config.go
│
ā”œā”€ā”€ Dockerfile
ā”œā”€ā”€ go.mod
└── go.sum

The same pattern applies to product-service/ and order-service/. The architecture stays the same. Only the deployment boundary changes.

              MODULAR MONOLITH
 
       ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
       │          API             │
       │                          │
       │ User │ Product │ Order   │
       ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
 
                   ↓
 
             MICROSERVICES
 
       ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
       │   User   │ │ Product  │ │  Order   │
       │ Service  │ │ Service  │ │ Service  │
       ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Microservice extraction becomes an incremental engineering operation, not a full rewrite.


When to extract a service

Extract a domain to its own deployable only when there is a concrete reason:

  • Independent scaling (CPU-heavy workload)
  • Separate release cadence or team ownership
  • Different SLAs or data residency
  • Fault isolation that justifies the operational cost

Until then, keep domains in one repo with clear package boundaries. Premature splitting adds network latency, distributed transactions, and deployment complexity without benefit.


Code2b alignment

New Go services at Code2b should start with this layout. Existing services (enrichabl microservices, docvalidation, vistalink) may use older folder names (handler, usecase, repository) but follow the same dependency direction: handlers call business logic, business logic depends on interfaces, SQL and HTTP clients sit at the edges.

When adding a feature to an existing service, extend the matching domain folder first. Add a port in domain/, implement the adapter in outbound/, expose it through inbound/. Do not skip straight from handler to SQL.