Saltar a contenido

Week 17 - DDD in Go: Hexagonal Architecture, Bounded Contexts

17.1 Conceptual Core

  • Domain-Driven Design in Go starts with one observation: Go's package system is a bounded-context tool. A package can hide types, expose only the interfaces consumers need, and the import graph enforces direction.
  • The hexagonal pattern in Go:
  • Domain package: pure types, behaviors, ports (interfaces) for external dependencies. No imports from net/http, database/sql, etc. This is the dependency-direction rule.
  • Adapter packages: one per external system (postgres, kafka, http-client). Each implements the ports the domain defines.
  • Application package: use cases-methods that orchestrate domain operations across adapters.
  • Cmd package: composition root. Wires adapters into a runnable binary.
  • The three Go-specific hazards:
  • Anaemic domain: types are bags of fields with all logic in services. Push behavior into the type.
  • Receiver-method abuse: mutating methods on value receivers (compile-pass, semantic-fail). Pick T vs *T deliberately.
  • internal/ not used: Go's internal/ directory restricts imports to subtrees. Use it aggressively to enforce layering.

17.2 Mechanical Detail

  • Layout for a hexagonal Go service:
    service/
      cmd/
        api/main.go              # composition root
      internal/
        domain/                  # pure types + ports (interfaces)
        application/             # use cases
        adapter/
          postgres/              # impl PostgresUserRepo
          kafka/                 # impl EventBus
          http/                  # impl HTTP handlers
        platform/
          observability/         # slog, otel, prom wiring
      pkg/                       # exported (rare; most things are internal/)
    
    Note internal/: nothing outside service/... can import it. This is the architectural test.
  • Defining ports as interfaces:
    package domain
    
    type UserRepo interface {
        ByID(ctx context.Context, id UserID) (User, error)
        Save(ctx context.Context, u User) error
    }
    
    Define interfaces where they are consumed (in domain or application), not where they are implemented. This is "consumer-defined interfaces," the Go counterpart to dependency inversion.
  • Errors as domain values: a domain error like ErrUserNotFound = errors.New("user not found") plus var ErrUserNotFound for errors.Is matching. Adapter packages translate sql.ErrNoRows to domain.ErrUserNotFound at the seam.
  • Avoiding leakage: never let a *sql.Tx or a *http.Request cross into domain. The compiler will not stop you; the architectural test will.

17.3 Lab-"A Hexagonal URL Shortener"

Build a workspace implementing a URL shortener: - internal/domain -ShortURLaggregate,URLRepoandHasherports. -internal/application - Shorten and Resolve use cases. - internal/adapter/postgres - implementsURLRepoagainst a real Postgres (usepgxnotdatabase/sql). -internal/adapter/http - REST handlers using application. - internal/adapter/memory - in-memoryURLRepofor tests. -cmd/api - wires everything.

The architectural test (a Go test) walks the import graph and fails if internal/domain imports any adapter package or stdlib networking package.

17.4 Idiomatic & golangci-lint Drill

  • depguard (forbids cross-layer imports), revive: empty-block, gocritic: dupCase. The depguard rules become the executable architecture documentation.

17.5 Production Hardening Slice

  • Add depguard rules forbidding internal/domain from importing net/http, database/sql, context.Background, and any third-party adapter packages. CI fails on a violation. This is the architectural test in lint form.

Comments