In this article, I will share my approach to structuring Go applications using DDD principles, and highlight some of the tradeoffs that come with this approach.
By following the principles outlined in this article, you can create a structure for your Go application that separates concerns between the domain layer, the application layer, and the infrastructure layer making your application more modular, easier to test, and more maintainable over time still maintaining idiomatic Go packaging.
This approach reflects my personal opinion and is based on my experience and the tradeoffs I faced during my years of experience. While this approach may not be the only way to structure a DDD-based application in Go, it is one that I have found effective in maintaining truly idiomatic Go applications.
NOTE: I also suggest you spend some time looking at the linked example application links to better help you to deepen your understanding of the concepts and principles discussed in my article,
By examining the codebase in detail, and experimenting with the various components and interactions, you can gain a more concrete and hands-on appreciation of the practical applications and benefits of Domain-Driven Design in Golang. Moreover, by exploring the code examples provided, you can gain insights into best practices, common patterns, and potential pitfalls to avoid when working with DDD and Golang.
In the realm of DDD software development, the domain layer serves as the foundation for defining fundamental concepts, regulations, and processes that apply to a particular problem domain.
The layer should not rely on external implementation details and should function independently.
For instance, if you are designing an e-commerce application,
your domain layer would contain core concepts such as
which would operate autonomously from any infrastructure concerns.
When organizing Go applications using the Domain-Driven Design (DDD) approach, I advocate that the root of the repository should contain domain types and domain logic (except for non-Go files), so that other developers can import them as dependencies when needed, utilizing the language’s importing mechanisms.
Let’s consider an “order-service” repository as an example; it could look like the following:
📂 order-service ┣ 📂 config // you can place here configuration files, Dockerfiles, migrations files, etc. ┣ 📄 docker-compose.yaml // you can place some other non-Go files here as well, up to you and your team ┣ 📄 go.mod ┣ 📄 go.sum ┣ 📄 id.go // contains the value type (AKA value object) for representing an order's id ┣ 📄 order.go // contains the aggregate root representing an order ┣ 📄 repo.go // contains the interface(s) for the repository pattern ┣ 📄 repo_mock.go // contains the generated mock(s) of the interface(s) contained in the repo file ┗ 📄 status.go // contains the value type (AKA value object) for representing an order's status
Note: you can navigate the content here.
Despite some people finding the
repo_mock.go files unattractive,
I have made a conscious decision based on my years of experience.
The repository interface is an integral aspect of the domain contract,
utilizing domain types and presenting an API that adheres to the domain’s ubiquitous language.
Consequently, I situate
repo.go in the domain layer,
which includes only the interface(s) and the errors it exposes.
Along with this strategic rationale, I also prefer to place repository mocks in the domain layer to minimize unwanted complexity, despite not being a part of the domain.
While I have attempted to generate mocks solely in the packages where they are required previously, I discovered it to be more challenging to maintain, and thus opt to keep the mock there.
The Application Layer serves as a lightweight layer of code for controlling the flow of data between the Domain Layer and the Infrastructure Layer; by its nature, this layer should not contain any business logic and should always delegate this responsibility to the Domain Layer.
To maintain the validity of this premise when dealing with the Application Layer in a Domain-Driven Design (DDD) and Golang context, my objectives are twofold.
Firstly, I strive to enforce a strict separation of concerns by ensuring that the domain logic does not directly depend on this layer. This helps to preserve the autonomy and encapsulation of the domain layer, which is critical for maintaining the integrity of the domain model.
I can ensure this by adding the Application Layer as an
internal package part of the repository root.
This approach allows me to leverage Go’s cycle dependency checks,
which ensure that the domain never imports the
internal package but only the opposite.
By establishing this unidirectional flow of dependencies,
I can maintain a clean and well-defined separation of concerns between the two layers.
This practice also enables me to establish a clear boundary between the
internal repository implementation
and the public interface that the domain layer relies on,
promoting a higher degree of decoupling and flexibility in the system design.
Secondly, I aim to keep the Application Layer as lightweight as possible to minimize any unwanted complexity arising from bloated code. By adopting a lean approach, I can help to reduce the cognitive load required to understand and maintain this layer, while allowing it to handle cross-cutting concerns like transactions, security, and other infrastructure-related tasks.
To achieve this, I avoid introducing extraneous use-case-specific input types such as “Commands” or similar types, which can add unnecessary complexity and dependencies to the system design. Instead, I strive to create a seamless integration between the Application Layer and the domain layer, using domain types as the primary communication and collaboration between the two layers. This helps to simplify the system architecture, improve code maintainability, and promote a more cohesive and unified domain model.
As the complexity of the application layer increases within broader and intricate domain boundaries, maintaining the second objective becomes progressively arduous. Adopting a structured approach in such scenarios is crucial to simplify the complexity of the APIs (methods and functions) and to maintain a balance between the two objectives.
Iterating over the previously introduced “order-service” example, the outcome of it may look like this:
📂 order-service ┣ 📂 internal ┃ ┗ 📄 service.go // contains the application service implementing the use cases owned by the domain boundary ┣ 📂 config // ... ┣ 📄 docker-compose.yaml // ... ┣ 📄 go.mod ┣ 📄 go.sum ┣ 📄 id.go // ... ┣ 📄 order.go // ... ┣ 📄 repo.go // ... ┣ 📄 repo_mock.go // ... ┗ 📄 status.go // ...
Note: you can navigate the content here.
The infrastructure layer plays a crucial role in defining the technical details of how your application interacts with external systems such as databases, message brokers, HTTP handlers, and third-party APIs. It sits on the application layer and interacts with it to fulfill technical requirements.
It is essential to note that the infrastructure layer should focus solely on technical concerns and should not contain any business logic.
I recommend that the Infrastructure Layer is strategically placed in a dedicated
located at the root of the repository.
By centralizing the Infrastructure Layer in this manner, developers can benefit from a simplified and streamlined architecture.
This is because the
cmd package can serve as a hub or focal point for all infrastructure-related components,
making it simple to modify and optimize various aspects of the system as necessary
without impacting the domain layer.
In addition to this,
I strongly suggest that developers create an
internal package within the
to house reusable components that are shared across multiple binaries.
This could include packages such as databases, cache, message brokers, etc.
By centralizing these reusable components in a dedicated
developers can benefit from enhanced efficiency and consistency in their codebase.
Moreover, it facilitates the seamless sharing of resources and assets across multiple binaries,
enabling developers to avoid duplicating code unnecessarily and thereby reducing the risk of errors and inconsistencies.
Overall, these best practices can significantly enhance the maintainability and design of software systems, resulting in a more robust and effective software development process.
📂 order-service ┣ 📂 cmd ┃ ┣ 📂 http-d ┃ ┃ ┗ 📄 main.go // A binary that exposes an HTTP server and offer behavior accessible via network ┃ ┣ 📂 cli ┃ ┃ ┗ 📄 main.go // A binary that exposes an application and offer behavior accessible via input ┃ ┗ 📂 internal ┃ ┗ 📂 repo ┃ ┣ 📂 cache ┃ ┃ ┗ 📄 cache.go // A cache implementation(s) of the interface(s) placed in repo.go ┃ ┣ 📂 instrument ┃ ┃ ┗ 📄 instrument.go // A tracing/metrics/logging implementation(s) of the interface(s) placed in repo.go ┃ ┗ 📂 postgres ┃ ┗ 📄 postgres.go // A postgres implementation(s) of the interface(s) placed in repo.go ┣ 📂 internal ┃ ┗ 📄 service.go // ... ┣ 📂 config // ... ┣ 📄 docker-compose.yaml // ... ┣ 📄 go.mod ┣ 📄 go.sum ┣ 📄 id.go // ... ┣ 📄 order.go // ... ┣ 📄 repo.go // ... ┣ 📄 repo_mock.go // ... ┗ 📄 status.go // ...
Note: you can navigate the content here.
Difference to consider when using a monorepo
I am keenly aware of the unique challenges associated with designing and implementing complex software systems that operate within the context of multiple subdomain boundaries; in particular, creating an efficient packaging structure can be tricky when developing a monorepo that owns several subdomains.
However, by using multiple packages to split domain boundaries and still embracing the design principles presented for each layer, developers can create a structure that effectively segregates and compartmentalizes different areas of the system, enhancing maintainability and scalability.
In my experience, this approach has proven highly scalable and robust, even in the most complex and demanding software development environments.
An example package structure looks like this:
📂 monorepo ┣ 📂 cmd ┃ ┣ 📂 order-http-d ┃ ┃ ┗ 📄 main.go ┃ ┃ ┗ 📂 internal ┃ ┃ ┗ 📂 repo ┃ ┃ ┣ 📂 instrument ┃ ┃ ┗ 📂 mysql ┃ ┗ 📂 user-http-d ┃ ┗ 📄 main.go ┃ ┗ 📂 internal ┃ ┗ 📂 repo ┃ ┣ 📂 cache ┃ ┣ 📂 instrument ┃ ┗ 📂 postgres ┣ 📂 internal ┃ ┣ 📂 order ┃ ┃ ┗ 📄 service.go // and other application-layer files ┃ ┣ 📂 user ┃ ┃ ┗ 📄 service.go // and other application-layer files ┃ ┗ 📂 product ┃ ┗ 📄 service.go // and other application-layer files ┣ 📄 go.mod ┣ 📄 go.sum ┣ 📂 order ┃ ┗ 📄order.go // and other domain-layer files ┣ 📂 user ┃ ┗ 📄user.go // and other domain-layer files ┗ 📂 product ┗ 📄product.go // and other domain-layer files
If you are interested in exploring my overall approach, I recommend you two articles that I have written on the topic, they provide a detailed and nuanced explanation of my thought process and methodology, and are intended to offer readers a deeper understanding of my perspective.
The links to these articles are as follows:
I’d also love to hear your thoughts and opinions, whether you agree or disagree with my approach.