My Thoughts on Domain-Driven Design in Golang
My Thoughts on Domain-Driven Design in Golang#
Contrary to what most people think, DDD is not about folder naming. It is not about having directories called domain and infrastructure and calling it a day.
Here is my lightweight approach that helps you think about what your app domain actually is.
The concept is straightforward: you see a domain, you create a directory for it. The real value is not in the structure itself — it is in the thinking the structure forces you to do.
What DDD Actually Sells#
Two things, done well, pay for all the complexity:
Clear separation of concerns. Business rules live in the domain layer. They do not leak into HTTP handlers, database queries, or Kafka consumers. When you need to change a business rule, you know exactly where to go.
Testability. When your domain is isolated from infrastructure, you can test it without spinning up a database. Unit tests become fast and deterministic. Integration tests cover the seams, not the logic.
Those two things alone justify the pattern — if you apply it with discipline.
What Actually Goes Wrong#
I have had enough conversations with engineers who have worked with DDD in practice to know that the theory and the reality diverge quickly.
The most common failure: someone put a struct in the root package and made everyone refer to it.
You get a root-level models package that becomes a catch-all. Every service imports it. The package grows. Fields that started with clear meaning blur over time. Engineers become afraid to reuse existing fields — because they are not sure who else is depending on them — so they duplicate instead. You end up with bloated interfaces, partial updates, and fields that mean slightly different things in different contexts.
People lost track of what was where. The package was supposed to create clarity. It created a sprawl of hefty dependencies instead.
The second failure: treating DDD as a replacement for thinking.
In one team, DDD led to extremely convoluted hierarchies of interfaces and types — premature abstractions for things that were either two years out or never going to come. Abstractions for all sorts of API transport layers, when all they needed was an HTTP API. A models package full of types that would have been better defined close to where they were used.
That may have been an artefact of the person who introduced it being “young, motivated, but inexperienced.” But it soured the whole team on the concept — which was not the concept’s fault.
The third failure: consensus.
The most difficult part is not the code. It is getting a team to agree on how to organise files and packages. Once that consensus breaks — once one person interprets “domain layer” differently from everyone else — you get a hybrid that has all the overhead of DDD and none of the benefits.
A Practical Structure That Works#
If you want to try it without going overboard, here is a structure that makes sense:
/infrastructure — config, database connections, cache setup, bootstrap
/interfaces — controllers, presenters (HTML/JSON)
/services — use cases and repositories
/datamodels — aggregates and domain types, close to their use
infrastructureholds anything that touches the outside world: config readers, DB connections, cache clients, the router setup, main bootstrap. One-time initialization.interfacesholds controllers and presenters. It routes requests to the right service method and presents the output.servicesholds use cases and repository interfaces. Business logic lives here.datamodelsholds your aggregates — structs that represent business concepts and can be manipulated and presented.
If you have a large project, you can add subfolders inside interfaces/controllers/ — for example interfaces/controllers/finance. Do the same for services and repos. But only do that when there is a real need, not before.
For shared utilities — error types, logging helpers, things needed across the whole program — root/common is fine. When you start a second project, some of those common things might move to a shared library. But not before you need it.
The Onion / Hexagonal Framing#
What I described above is essentially what people call onion architecture or hexagonal architecture. DDD, applied practically, tends to land in the same place.
The flow looks like this:
Infrastructure / Initialise
↓
Infrastructure / Web hit → routes to controller method
↓
Interfaces / Controller → points to use case method
↓
Services / Use Case → gets data from repository method
↓
Services / Repository → gets data from DB, all the way back to controller
The domain provides business logic and uses abstractions. Infrastructure implements those abstractions. The application layer (interfaces) combines them and exposes the result to the user.
You are probably already doing something close to this. Most engineers are. The DDD label is just a name for the instinct that says “keep business rules away from database queries.”
My Opinion#
It depends on the size of the project.
If you start small, start simple. Do not overthink the structure. A flat layout with a few directories — handlers, services, store — is fine for a small service. Forcing DDD on a three-endpoint API is the fastest way to spend a week on architecture and zero time on the actual problem.
If you are building something that will grow, or that needs a team to work on it independently, DDD gives you a coordination mechanism. When the domain layer is well-defined, a new engineer can work on a use case without understanding the full stack. When you want to extract a service later, the boundaries are already drawn.
There is no one-size-fits-all. The best teams take what they need and leave what they do not. You do not have to be a purist. Structure the application using the layers that make sense for your context, use plenty of abstractions where they help, and skip them where they do not. There is no rule that says you must follow the full DDD playbook to get value from the thinking behind it.
The concept is sound. What gets people into trouble is applying it too literally, too early, or without team consensus on what the words mean.
Start small. Refactor when the structure earns it. Do not abstract things that do not exist yet.