Back to Home

DDD and Clean Architecture in Go: pragmatic approach

The article explains how to pragmatically apply DDD and Clean Architecture in Go, avoiding excessive structure. Code examples, project organization recommendations and key rules for reducing technical debt are provided.

Go: DDD and Clean Architecture without 200 files
Advertisement 728x90

# 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:

Google AdInline article slot
  • A dto folder with duplicate structs.
  • mappers between layers for trivial conversions.
  • Interfaces in domain that 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.

Google AdInline article slot

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.

Google AdInline article slot

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, not infrastructure.
  • No dto or mappers folders unless truly needed: reuse the same structs across layers if it doesn't violate boundaries.
  • Business logic only in domain. For example, Cancel() method for Order checks 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 dto if structs align.
  • Aggregates define transaction boundaries. Change only through the root for consistency.
  • Testing—domain tests run without infrastructure, speeding up development.

— Editorial Team

Advertisement 728x90

Read Next