Background
In the places where I develop and operate systems, we’ve recently been adopting domain-driven design. At the same time, systems devops has few laws you could call absolute; almost everything has upsides and downsides (= there is no silver bullet).
In this article I organize and summarize my own understanding of Domain-Driven Design and onion architecture.
Summary (TL;DR)
- In the devops of business-facing systems, one important factor is that “system behavior is easy to imagine from the source code”
- Domain-driven design isn’t tied to a specific architecture, but adopting onion architecture makes it easier for the team to understand “which code plays which role”
- It isn’t a silver bullet, but if you use “domain-driven design + onion architecture” well in development, you can expect to reduce the devops team’s cognitive load
On Adopting Domain-Driven Design and Onion Architecture
What Matters in Systems devops
First, one important thing in developing business-facing systems is that “system behavior is easy to imagine from the source code.” There are many other important things, but in this article I take this one up first, as it relates to design approaches and architecture.
Why does it matter that “system behavior is easy to imagine from the source code”? Because systems frequently need changes in response to business requirements, and a system whose behavior after a change is hard to imagine becomes painful to operate. Conversely, source code that will never be changed again (embedded software, for example) may look somewhat complicated, but since it won’t be modified as long as it behaves as expected, there are cases where you don’t need to worry about it much.
So how do you make “system behavior easy to imagine from the source code”? I think it comes down to “making the dependencies within the system explicit so that the system’s structure can be understood from the source code.”
For example, suppose the system structure is a tree like the following.
A system is “a state machine that holds state and returns output for a given input,” so if you can understand from the source code that the system structure looks like the figure above, imagining its behavior becomes easy.
On the other hand, suppose the system structure looks like the following (the so-called Big Ball of Mud).
In this case, when you modify part of the program, the dependencies are complex enough that imagining where the impact propagates is difficult (though it depends on the scale of the system, the developers’ skill, and the size of the team).
Domain-Driven Design
Domain-driven design is an approach to designing software with domain knowledge (the business knowledge and rules being turned into software) at its core.
Quoting from Domain Driven Design Quickly:
So how do we create software that fits the domain smoothly? The best way is to make the software a reflection of the domain. To do that, the software needs to incorporate the core concepts and elements of the domain, and accurately reproduce the relationships between them.
Why do we develop and operate software in business? In most cases, to make work more efficient (note 1).
Because domain-driven design is a design approach that turns real-world domain knowledge into software as its core, reading well-organized domain layer code makes it easy to imagine what it’s trying to do and how it behaves (note 2). If “system behavior is easy to imagine from the source code,” then modifying code when a new business flow is added, or understanding a problem when the system behaves unexpectedly, becomes easier.
There are many books and web articles on domain-driven design worth referring to, so I’ll omit the details.
- Note 1: Not all software development fits this. For software that doesn’t aim at business efficiency — compilers, the Linux kernel, and so on — there’s no need to adopt domain-driven design
- Note 2: As an exception, if you have to develop with no knowledge at all of the domain you’re trying to address (for example, in contract development), then even reading the domain layer code may leave behavior hard to imagine, since you lack knowledge of the domain. In such cases too, domain-driven design may not be a good fit
Onion Architecture
Domain-driven design is only a design method that puts the domain at the center; it doesn’t prescribe the system’s architecture. DDD Reference does take up Layered Architecture, and states the key point as follows.
Isolate the expression of the domain model and the business logic, and eliminate any dependency on infrastructure, user interface, or even application logic that is not business logic.
In other words, as long as you can make the domain model and business logic independent, the architecture can be anything.
In fact, there are examples of domain-driven design implemented with clean architecture, hexagonal architecture, or in functional languages.
One such architecture that I often use is Onion Architecture (note 3). Strictly speaking, it’s the architecture in the figure below, with the original onion architecture’s naming changed.
The reason I use the architecture above is that I feel it has the following benefits.
- Compared with the Layered Architecture described in DDD Reference, the domain layer doesn’t depend on the infrastructure layer and can stand independently
- Compared with Clean Architecture, it’s simpler, and if applied appropriately across multiple components it can reduce a team’s learning cost and cognitive load (like Rails’ MVC framework)
Especially when doing devops as a team, striving to implement according to a diagrammed architecture makes it easier for newly joined members to “imagine system behavior from the source code.”
- Note 3: Comparisons with other architectures are concisely summarized in “Chapter 5 - Architecture” of Domain-Driven Design Modeling/Implementation Guide - little-hands, so if you’re interested, give it a read
Structure in Go
Having explained domain-driven design and onion architecture, let me summarize what I actually do when developing in Go.
The basic directory structure I often use looks like this. I sometimes adjust it depending on the complexity of the domain being handled.
| |
Key Points
- Layers made explicit by directory separation. Packages inside domain must not call infra or usecase
- Put cross-cutting system functionality that isn’t exposed externally — logging, metrics, and so on — in the internal directory. In Go, things under the internal directory cannot be referenced from other packages
- The domain package may reference internal, but keep it to a minimum to increase the domain’s independence. In the worst case domain logic leaks into internal, or you end up depending on logic unrelated to the domain, so use it with care
Closing
To repeat, domain-driven design is one method of system design, and it doesn’t fit all system development well. What’s asked of a software engineer is to understand what you want to achieve along with the trade-offs, and to consider using domain-driven design in situations where it looks like a good fit.
References
- DDD Reference - Domain Language
- Domain-Driven Design Modeling/Implementation Guide - little-hands - BOOTH
- The Onion Architecture : part 1 | Programming with Palermo