The least-discussed but structurally most fundamental pattern concerns our Modules and the structure these put on our work.
Modules use ubiquitous language to express the technical side of the domain.
There is no "one way" to use Modules, it's all mostly about naming and organizing your code. Organize your code in Modules by using concepts like namespaces, classes, folder structure, and even how you split responsibilities across microservices. The goal is to accurately express the domain by the way you have structured and named things.
In the DDD context, we use Modules as a logical construct to segregate between concerns when we technically implement our domain model. Modules should precede the Bounded Contexts because Modules typically reside in the same codebase and reflect the logical model of our domain. Dividing logical wholes into separate Bounded Contexts can cause problems (Vernon 2013, p. 344). One example of a valid use is to reach for Modules if you need to create a second model in the same Bounded Context (Vernon 2016, p.50).
Again, Modules are local to the code, while Bounded Contexts may constitute one or more logical solutions. Yet these both (in particular Modules) share the common trade-offs of public interfaces:
[E]ffective modules are deep: a simple public interface encapsulates complex logic. Ineffective modules are shallow: a shallow module's public interface encapsulates much less complexity than a deep module.— Learning Domain Driven Design (Khononov 2021, p. 223)
This is the most basic tactical pattern, yet it at heart is all about classic programming concepts like "high cohesion, low coupling" and, as per DDD, expressing the Domain through the naming and functionality.
With all this said, though, the Module pattern itself is not descended from DDD; it is a common pattern that has been around probably since the start of at least object-oriented programming. We use this pattern to encapsulate and, sometimes, name some part of our application. This can be done by language-specific mechanisms and/or by structuring our code in files and folders.
In terms of ontology, a Module can be a namespace or a package, depending on the language that you are using. For our example code, using TypeScript, there do exist mechanisms to handle this, but they are not completely idiomatic to how the language is typically used. Instead, we will have to do this only at the file and folder level. Generally, it does make sense that we should also see the structure and folders as a related effect of our Modules. Therefore Modules are not simply only a technical matter, but a logical matter.
Much of DDD wisdom and attempts at concretely structuring files in a DDD-leaning sense will address why one of the most basic tactical things we can implement is packaging by Module (or features) rather than by layers. You'll perhaps already have experience seeing how many trivial or common projects will use the layered, format-based approach, segmenting folders into their respective types (especially common in front-end projects) or using vague, non-descriptive categories such as
helpers. This makes it very hard to understand how objects and functions relate and what their respective hierarchies are. It also becomes hard to discern the domain logic from the overall structure, the Module names, and their usage. All that becomes much easier with Modules.
For more, from a non-DDD angle, read this article about why packaging by feature is better than packaging by layers.
In DDD you'll hear a lot of arguments against importing outer-level objects (such as services) into deeper-level objects, such as Aggregates. This is sound advice, generally speaking. If we start importing left-right-and-center without discipline we will end up in a really bad place!
It's worth noting that DDD itself is not prescriptive at all regarding how to set your file structure. In fact, there is practically nothing in Evans' book about this. Obviously, it does make sense to somehow reflect the "methodology" in how the actual code is organized, but DDD won't save you here, I'm sad to say. Clean Architecture, though, will paint a much more exact idea, itself borrowing from the Ports and Adapters (or onion/hexagonal architecture) notion.
There are several examples out in the wild that aim to present various individuals' takes on DDD, in particular, and some Clean Architecture, generally. Sometimes you may find these combined as I have done, but that's typically not quite as common.
Reasons I don't necessarily like some of the other examples out there, include:
- Overbearing amount of boilerplate and folders.
- Typically oriented toward monolithic use cases or indistinct deployment models.
- Related to the above: Over-modularization, where I believe microservices themselves should be the first module boundary.
- Use of decorators; is something that is not standardized in TypeScript (Vandenkam 2021, p. 197-198).
- Use of inversion of control (IoC) libraries and dependency injection (DI) containers/libraries rather than using the language features provided, or simply using regular object-oriented programming. These needs can be handled without external library dependencies by using higher-order functions or passing in dependencies in a functional way.
- Intricate uses of more complex ideas like monads (such as
Right) which adds a higher threshold than necessary for people to start getting value from tactical DDD.
All of these concerns are addressed and "taken care of" in the example code that goes with this book.
Taking DDD and CA together, we get a pretty powerful toolbox. You should understand that many examples are based on monolithic applications, something I personally very rarely work on. The example here addresses a microservice perspective. The bounded context itself is the main feature, so to speak.
Clean Architecture also changes the structure and naming a bit. We will base our core understanding of application structuring on Clean Architecture and its respective nomenclature, as it's more prescriptive than regular DDD.
As always, "Don't try to be clever". DDD is hard enough as it is, so it makes sense to be pragmatic and functional.
In our case, the principal module structure for code is:
code/Reservation/Reservation:The reservation solution and Bounded Context (core subdomain)
code/Reservation/Display:The display solution and Bounded Context (supporting subdomain)
code/Analytics/Analytics: The analytics solution and Bounded Context
code/VerificationCode/VerificationCode:The verification code solution and Bounded Context
Here we've almost completely nailed the 1:1 relationship between Bounded Context and subdomain, as well as have a top-level modularization of solutions/code into these.
The "Clean Architecture" is a relatively well-known variant of the onion/hexagonal/ports-and-adapters school of architecture.
Many have tried and many have failed when it comes to setting up a folder structure for DDD. For my part, I've found that Robert C. Martin's "clean architecture" is a better (and simpler!) elaboration of where so many developers have tried to find a way. It's not magic, just a very nice mapping (and blog article, and book for that matter!).
From Robert C. Martin's blog. "The Clean Architecture", 10 August 2012. https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
I find it the most immediately effective and neat variant of these, as it:
- Introduces very little in terms of novel concepts;
- Is almost directly compatible with how DDD envisions structure in the software realm;
- Powerfully exploits the dependency rule for well-working and testable software.
Robert Martin writes about the dependency rule like this:
The concentric circles represent different areas of software. In general, the further in you go, the higher level the software becomes. The outer circles are mechanisms. The inner circles are policies.The overriding rule that makes this architecture work is The Dependency Rule. This rule says that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle. In particular, the name of something declared in an outer circle must not be mentioned by the code in the inner circle. That includes functions, and classes. variables, or any other named software entity.By the same token, data formats used in an outer circle should not be used by an inner circle, especially if those formats are generated by a framework in an outer circle. We don’t want anything in an outer circle to impact the inner circles.
The intention with all of these ideas for how to structure an application is all well-meaning, but I've also seen and reflected on how a higher level of "layers" or "circles" can complicate things quite quickly.
Let's at least look at the levels and some examples of what would go into each, respectively.
- Entities: "Business objects of the application"
- Use cases: "Use cases orchestrate the flow of data to and from the Entities, and direct those Entities to use their enterprise wide business rules to achieve the goals of the use case"
- Interface adapters: "A set of adapters that convert data from the format most convenient for the use cases and Entities, to the format most convenient for some external agency such as the Database or the Web"
- Frameworks and Drivers: "Where all the details go. The Web is a detail. The database is a detail. We keep these things on the outside where they can do little harm"
Ultimately: The farther in something is, the less likely it is to change. Any inner layers must not depend on the outer layers.
I will apply a set of small modifications to this just to juice it up even more. Some of the names from above are too narrow ("entities") and some are just weird when used in everyday work ("frameworks and drivers"). We can also steer it a smidge towards the DDD nomenclature, and we would arrive at this concept:
Our adjusted model that will follow the overall Clean Architecture outline. The Upper (bigger) names represent the layer name, and the lower (smaller) represents examples of what goes into the layer.
Or in tabular form with the actual folder names too:
Frameworks & Drivers
Application Business Rules
Enterprise Business Rules
You will notice that here adapters are part of the infrastructure layer rather than being on their own.
If we use a tool like Madge to generate a diagram of the code, we should be able to see the same acyclic flow that we want (given that we actually also write the code in the "clean" way!). Below is an example of the
Note that the above diagram excludes certain code paths—such as those which represent test data, utilities, and interfaces—which can either be used across all levels (or "depths") or add no meaningful detail to the diagram. They just make the diagram overly busy and are an acceptable omission.
Now, this I am happy with!
The "grown up" way to think about infrastructure is that they are generic functions, classes, and objects that help set up non-domain-related functionality. Good examples of this include Repositories, very generic utility functions, Lambda event handlers (the outer layer), and anything else that has no (or very little) unique value in the specific context.
I've been totally happy with not using Clean Architecture's "frameworks and drivers" nomenclature here but keeping it very flat and simple instead. Those terms didn't really stick with me or become communicated very well. It's fine that frameworks and drivers are part of the infrastructure, but I've personally abandoned packaging under that name.
For me, a useful heuristic has been "Can I move this thing without making essentially no changes and still get it working?". That does however maybe also say something about the desired level of quality, too...
Adapters are part of the infrastructure layer, because, well...they are infrastructure.
In the application layer, we put anything that is not core to the business, but which does have unique value. This should be the first layer where something "new" happens while all the code running before this layer could theoretically be a basic boilerplate.
We still use the concept of "use cases" and they go into this layer.
Now for the crème de la crème, the secret sauce, and the figurative room where the magic happens. This is, as expected, where all the snazzy unique business logic and domain-orientation truly happens.
Interfacesis an additional folder that I tend to keep at the root—it just collects the types and interfaces. The reason I set this as a root-level item is so that we can effectively do things like:
- Exclude the folder when rendering dependencies
- Put them in the least nested and separate part of the overall structure, as practically every file will have to use some interface or another
- Get them out of the way while still actually putting these in their own place