Domain-Driven Design in Go: How Not to Drown in Layers of Abstraction

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Your Go Project Needs DDD?

\n\n

Have you ever written a Go service that started with a couple of endpoints and three database tables, only to turn into a monster six months later with mile-long handler.go files and magic numbers scattered throughout the code? Sound familiar. I've been through it too.

\n\n

The problem with classic Go development is that we think about databases and JSON too early, forgetting the most important thing: business logic. Domain-Driven Design (DDD) comes to the rescue precisely when you want to bring order to the chaos. But how do you apply DDD to Go when the language doesn't spoil us with classes and inheritance? Let's figure it out.

\n\n

In this article, I'll show you how to apply Domain-Driven Design in Go projects without excessive fanaticism, with working code examples and a clear structure. You'll learn how to separate the wheat from the chaff and write code that is understandable not only to the machine but also to the business analyst.

\n\n

What is DDD and Why Does a Go Developer Need It?

\n\n

Domain-Driven Design is not about technology. It's about language. It's about making the code speak the language of the business. If your domain is an online store, the code should have Order, Product, Cart, not DataTransferObject and EntityManager.

\n\n

Go, with its simplicity and lack of magic, is ideal for DDD. There are no proxy classes or reflection here that hide the essence. In Go, you are forced to be honest: if you have an aggregate, it's an explicit struct; if you have a repository, it's an interface.

\n\n

The main building blocks of DDD that we'll look at:

\n
    \n
  • Entity — an object with a unique identifier (e.g., User).
  • \n
  • Value Object — an immutable object describing characteristics (e.g., Address).
  • \n
  • Aggregate — a cluster of entities that guarantees consistency (e.g., Order with OrderItem).
  • \n
  • Repository — a collection of objects that hides storage details.
  • \n
  • Domain Service — an operation that doesn't fit into a single entity.
  • \n
\n\n

Now let's translate this into Go.

\n\n

Project Structure: How Not to Get Lost in Folders

\n\n

The good news: Go doesn't impose any structure. The bad news: without structure, you'll quickly drown. For DDD in Go, I recommend the following code organization:

\n\n
your-project/\n├── cmd/\n│   └── server/\n│       └── main.go\n├── internal/\n│   ├── domain/          # Pure core: entities, rules\n│   │   ├── order.go\n│   │   ├── product.go\n│   │   └── errors.go\n│   ├── application/     # Use cases / application services\n│   │   ├── order_service.go\n│   │   └── product_service.go\n│   └── infrastructure/  # Databases, external APIs, HTTP handlers\n│       ├── postgres/\n│       │   └── order_repository.go\n│       └── http/\n│           └── handler.go\n└── go.mod
\n\n

Note: domain does not import infrastructure. This is the golden rule of DDD — the domain knows nothing about databases and HTTP. It lives in a vacuum of pure structs and interfaces.

\n\n

Writing the Domain: Order and Value Objects

\n\n

Let's start with the core. Suppose we are building an order management system. Here's what the Order entity might look like:

\n\n
// internal/domain/order.go\npackage domain\n\nimport (\n    "errors"\n    "time"\n)\n\ntype OrderStatus string\n\nconst (\n    OrderStatusPending   OrderStatus = "pending"\n    OrderStatusConfirmed OrderStatus = "confirmed"\n    OrderStatusCancelled OrderStatus = "cancelled"\n)\n\ntype OrderID string\n\ntype Order struct {\n    ID         OrderID\n    CustomerID string\n    Items      []OrderItem\n    Status     OrderStatus\n    CreatedAt  time.Time\n    Upd                

Blogs

Book Recommendations