How to use C# projects and namespaces in a DDD project?
C# Projects and namespaces are often neglected as a method to improve code readability. In reality, when used correctly, they are a really powerful tool. How to use them correctly? Make them "scream" about the project domain!
This post is a part of DDD-starter-dotnet project. We encourage you to watch:
So what does the architecture of your application scream? When you look at the top level directory structure, and the source files in the highest level package; do they scream: Health Care System, or Accounting System, or Inventory Management System? Or do they scream: Rails, or Spring/Hibernate, or ASP ?
- Uncle Bob
What we see after the first glimpse at the Solution Explorer. Probably that we have lots of projects. Often the project names are so long that their most interesting part is invisible. We also see that we have more than one Service and that we distinguish Frontend , Backend, Business Logic and Data Access Logic. What does this facts add to our domain knowledge about the project? Unfortunately nothing... They are obvious, expected and unimportant. So what are projects and namespaces for? For concisely expressed domain concepts.
First steps in improved readability
First and foremost the project names must be short. We should remove all the unnecessary information from them. Company or product name can be moved to the level of Solution. No need to repeat it for every project! Suffixes like Service or Model can also be skipped. On the Assembly name level we of course keep the prefix to ensure that the generated .dll files are unique. For example project Sales from Solution MyCompany.CRM after compilation will become MyCompany.CRM.Sales.dll
Reflecting the Bounded Contexts boundaries
Secondly the project name should describe some part of the business. Good thing is that usually the business names already are not too long (otherwise business people won't be able to spell them during calls and meetings) If the project names have 3 or even 5 parts (except the company name) there is a high chance that they originate form IT. Such names are noisy and should be shortened.
It turns out that DDD Bounded Contexts names are the best source of projects names. Typical Bounded Context examples are Sales, Warehouse, Delivery planning. In the minds of business this is often the least granular division so it ensures that projects will have appropriate (i.e. not too small) size.
Separate project for each layer
Third point - dependencies between projects are a great tool to protect correct dependencies between architecture layers. For simple, CRUD-like parts of the system one layer (and one project) is enough. For other, more complex parts, where we need a deep and independent model we should use Hexagonal / Clean Architecutre. In such cases one Bounded Context will be implemented by several projects.
In case of Hexagonal / Clean Architecture we should have one project for Domain layer (Sales.Domain) and Application layer (Sales.App). Adapters layer may have several projects depending of number of our system integrations. There should be a separate project for communication with the SQL DB (Sales.Adapters.Sql) and a separate one for the REST API (Sales.Adapters.RestApi).
References between projects should reflect the dependencies rules of architecture i.e. they should go inwards in the direction of the Domain Layer which is the heart of the system. This means that:
- Sales.Adapters.RestApi knows about Sales.App
- Sales.App knows about Sales.Domain
- Sales.Domain does NOT know about Sales.App or any concrete Adapter
- Sales.App does NOT know about any concrete Adapter
In addition, any concept which is not part of a given layer's API should have visibility
internal. This way we minimize chances that someone will try to reuse them in different context which will be completely against our architecture approach.
Ensure that Solution has the right size
What happens when we have very many projects in the Solution? First, we should ask ourselves a question how that happened. In theory it should not happen if we defined well-sized bounded contexts and used them as module boundaries as proposed above. In addition, general rule of thumb is that one Solution should be owned by one team. Other teams can also make small changes, but they should be reviewed by the owning team. Such work organization means that Solutions with very many projects (like 100 or even more) should not be seen at all.
But what if we need to deal with a monolithic application stored in one repository? In such case we should also strive to have the boundaries aligned with the bounded contexts. Such an architecture is called modular monolith. Its modules reflect the structure of business in a similar to microservices. In modular monotlith we can use multiple Solutions, each covering only a subset of all the projects in the system.
It may happen that after using all the above suggestions the number of projects in a single Solution will be still too large. In such case we can consider using Solution Folders. All projects belonging to a single bounded context should be grouped in separate folder. This way we keep using business names while improving ease of navigation even in a very big Solution
On the other hand it may happen that we will have one Bounded Context in one Solution especially when we don't deal with a monolith and we don't keep code in a single repo but we rather use micro-services. One Bounded Context in one Solution is a perfectly valid situation and we should keep applying all the previously mentioned rules . The biggest difference is that such a Solution would be very small, so looking at it we won't discover other Bounded Contexts present in the system.
Nasz kod powinien przede wszystkim nieść opowieść o biznesie który odzwierciedla. W Domain Driven Design jedną z podstawowych koncepcji, która w tym pomaga, jest tzw. Ubiquitous Language, czyli wszechobecny język. Jest to język wypracowany wspólnie przez biznes i IT, obecny we wszystkich aktywnościach i artefaktach, od spotkań z biznesem, przez modelowanie do kodu i dokumentacji. Nazwy w kodzie powinny więc być takie jak nazwy w biznesie! Podejście takie znacznie ułatwia nam komunikację, zmusza do precyzji i w ogromnym stopniu ułatwia utrzymanie systemu.
Majority of names in Ubiquitous Language will become types and method names in the code. But how about concepts which not singular and narrow as client or offer but are more broad and take many other concepts under the cover? Here we can use namespaces!
Names for Bounded Contexts and Modules
Namespaces have hierarchical structure which can be nicely used to reflect how business perceives the relationships between the most generic concepts and the more detailed ones. On the top level we should put the name of company or product. The next one should be reserved for BoundedContext - often the most generic name for certain part of business i.e. department or divisions where consistent, common language is used. As seen above - this name is also the name of the project.
Single Bounded Context is often big enough to further divide in smaller, more comprehensible parts. This parts in DDD are called Modules. For example in a context of Sales these could be Orders and Pricing. Inside Pricing we could have Discounts so the full namespace name would be MyCompany.CRM.Sales.Pricing.Discounts.
As the directory structure reflects the organization of namespaces we can browse through them and learn about the structure of business. If done well this can give more insights into the domain than any regular documentation.
Namespaces are not necessarily for layers
A co z tą częścią nazwy projektu która odzwierciedla podziały na warstwy np: .Domain, .Adapters.Sql ? Osobiście wolę nie dodawać ich do namespace'ów. Namespace'y niech niosą jedynie informację o podziałach biznesowych. To jaki jest podział na warstwy odzwierciedla już podział na projekty. Oczywiście typy ze wszystkich warstw dotyczące jednej części biznesu trafią wtedy do jednego namespace'a. Nie jest to jednak nic złego. Skoro jest to jeden fragment biznesu, to powinien być on wszędzie nazwany tak samo, niezależnie od podziału na warstwy, który zależy od innych czynników.
Avoid technical concepts in namespaces
How about using technical concepts like Controllers, Repositories, Entities as a part of namespace? This kind of information is pretty much a pure noise there. Such an approach leads to an uncontrolled growth of namespaces as all the controllers, repos or entities have to be squeezed in one place. In addition to analyze code regarding a certain part of business you need to navigate through a directory structure which is meaningless from the business point of view. In general - something to avoid at all cost.
Low coupling - High cohesion
Low coupling - High cohesion - nie ma lepszego podsumowania tych rozważań. Powinniśmy dbać o realizację tej zasady na każdym poziomie od typów do systemów. Jednym z poziomów jest właśnie poziom Bounded Contextów i Modułów. Powinny one mieć ściśle określoną, niezbyt szeroką odpowiedzialność i być na tyle niezależna na ile to możliwe. Odpowiedzialności i dopuszczalny stopień powiązań należy jednak zawsze ustalać na podstawie głębokiego zrozumienia biznesu, a nie kryteriów czysto technicznych!
Low coupling - High cohesion - it is hard to find a better summary for our suggestions. We should take care of them at every level of architecture - from single types to whole systems. Bounded Contexts and Modules are somewhere between them. They should have single, defined, not too big responsibility and be as independent as possible. Responsibilities and dependencies should be always defined with the deep understanding of business.
Using the suggested approach a glimpse on the Solution Explorer gives us idea about the business we operate in. Component names are short so navigation is easy. In addition every technical element of code has a clear responsibility and we now what should be expected from it. System is divided in a hierarchical way, according to real structure of the business. We use Low coupling on each hierarchy level. Let's summarize, once more, how to get there:
- All the names should be as short as possible and without noise (in a form of technical concepts).
- Company or product's name should be present in: Solution name (MyCompany.Crm), Assembly name (MyCompany.CRM.Sales.Domain.dll), Root namespace (MyCompany.CRM.Sales), but not necessarily in the project name (Sales).
- Project name should start from the name of Bounded Context (Sales or Sales.Adapters.RestAPI).
- Projects should reflect the architectural concepts (like having a project per layer). Dependencies should reflect the architectural style (domain is independent, application depends on the domain etc).
- Projects should expose only what is absolutely essential. By default we should use internal visibility level (not public).
- Namespaces should tell a story about business, not about technology. We should see there hierarchical Modules right after company and Bounded Context name (MyCompany.CRM.Sales.Orders).
- Namespaces should not contain names of architectural concepts which are already present in the project names (.Domain, .Adapters.Sql).
Architect, trainer, supporter of the Software Craftsmanship approach and close cooperation with business. He specializes in modeling based on Domain Driven Design and system architecture design.
He started his career in business consulting and then moved to IT. He worked both with enterprise class systems and with solutions for small companies. He even built his own startup product. Finally however he decided to stick with IT and share his knowledge and experience as a consultant and trainer.