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:
- Our code (give us a star 😉 ): https://github.com/itlibrium/DDD-starter-dotnet
- Our blog: https://itlibrium.com/tag/DDD-starter
- Twitter: ITLIBRIUM, Marcin, Szymon
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 do 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 towards an 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 be also 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 are not too long already (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 highest level of granularity 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 ensure correct dependencies between architecture layers. For example – simple, CRUD-like should have only one layer. For other, more complex parts, where we need a deep and independent model, we should use Hexagonal / Clean Architecutre. In the latter case one Bounded Context will be implemented by several projects. We should have one project for Domain layer (Sales.Domain), one for Application layer (Sales.App) and possibly many Adapters depending on 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) etc.
References between projects should reflect the dependency rules of chosen architecture. So in case of Hexagon 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 addtion, 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 reviewd 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 all the boundaries aligned with the bounded contexts. Such an architecutre is called modular monolith. Its modules reflect the structure of business in a similar way 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 a 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 but use micro-services. One Bounded Context in one Solution is then perfectly valid as long as we 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.
Our code should tell a story about the business it implements. Such approach, in Domain Driven Design is defined as Ubiquitous language. This language is a result of common business and IT efforts to describe concepts precisely. It is present literaly everwhere – at business meetings, in their minutes, during the modeling process, inside documentation. And in the code. First and foremost in the code! It makes communication much easier and forces us to be precise. As a result we get the high quality and easy of maintenance we want.
Majority of names in Ubiquitous Language 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 level 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. Quick reminder – this name is also the name of the project.
Single Bounded Context is often big enough to divide it further in smaller, more comprehensible parts. This parts in DDD are called Modules. For example in a context of Sales the Modules could be Orders and Pricing. Inside Pricing we could have Discounts so the full namespace name would be MyCompany.CRM.Sales.Pricing.Discounts.
The directory structure reflects the organization of namespaces, so 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
Tricky question: Shall we use the part of project name which describes architecture layer also in the namespaces inside it? For example: Put .Domain, .Adapters.Sql in the namespace names? Honestly, I do not recommend doing it. I prefer Namespaces to be focused only on structure of business. Projects are already a clear source of information about the layer we are in. Of course, types from various layers which touch certain part of business should be placed in the same namespace. It would mean we will have same namespace in different projects. But there is nothing wrong about that. If we implement logic regarding one, clearly defined part of business it should be named in each project (layer) in the same way. Having or not having these layers depends on a different drivers than structure of our business.
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. 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 – 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, well-defined, not too big responsibility and be as independent as possible. Responsibilities and dependencies should be always determined with a deep understanding of business.
When you’d use the suggested approach, even a short glimpse on the Solution Explorer will give you an idea about the business you operate in. Component names are short, so navigation is easy. 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. 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 use 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).
If you want to implement this approach quickly in your project take a look at our DDD-starter-dotnet project at Github.