Structuring Projects for Domain-Driven Solutions

Project organization is a topic that many developers speed through and eventually come to fight through as a project matures. The subject matter may seem mundane, but considering all of the other day-to-day responsibilities which consume time and impact deadlines, a poorly structured project is something that can have a minimized impact with just a small amount of planning. When implementing Domain-Driven Design, the organization of a solution into a series of specifically-purposed projects does not just reduce unforeseen development costs, it helps to enforce the required separation of concepts and responsibilities.

Non-Functional Requirements Are Requirements

One of the hardest battles that a developer faces when taking on architectural responsibilities is maintaining the importance of the role of non-functional requirements.  Non-functional aspects of the enterprise are critically important, but can be a tough sell.  Outside of the team, the value of non-functional aspects of development are often hard to justify.  Realistically, this is due to the fact that those are the parts of the system that cannot be interacted with and do not express an actual requirement of the business.  The problem is that they are an implied requirement, and while development can be done with a minimal amount of non-functional forethought, early gains are usually wiped out over time due to complexity, redundancy, and the fragility of the ever-growing codebase.

One common analogy that I like to use in these discussions, is that building software is like building a house.  The most important thing is that you build with a plan.  That does not mean that reams of paper need to be consumed to create archaic documentation that will never be read.  But, some sort of blueprint needs to be established and adhered to.  Lead developers and architects act as a foreman, not just overseeing who is working on particular parts of the plan, but that the plan is being followed to its ultimate vision.  The general architecture of the project is like the foundation of a house.  Once you start building the house, if you have neglected to lay a foundation first, it becomes increasingly more difficult to correct as the project moves forward.  Without the foundation, the walls will crack, the house will sink, and eventually, you are left maintaining the Winchester Mystery House.

"I know that these stairs lead nowhere, but we really wanted them!"

“I know that these stairs lead nowhere, but we really wanted them!”

Get too far, and that house needs to get torn down and built from the ground up again, hopefully learning the lessons from prior mistakes.  Likewise, the foundation does not just need to exist, it needs to be sturdy enough to support the burden of the weight put upon it.

In hypothetical discussions related to Domain-Driven Design, there are generally four “buckets” that concepts are placed in:

  • Domain – The Domain is where the actual Domain lives.  This encompasses the Domain, Subdomains, Bounded Contexts, Events, Aggregates, Entites, and Value Objects.  Additionally, it defines the interface contracts that the Domain requires for implementation which are infrastructure-bound.  This includes definitions for Repositories, Domain Services, Event Handlers, Commands, Queries, and much, much more.
  • Infrastructure – The Infrastructure relates to the physical interaction between the Domain and resource, most of the time being external subsystems which are not directly related to the Domain.  Prime examples of Infrastructure concerns are interactions with email dispatch, logging, and other cross-cutting concerns.  Very often, the Infrastructure wraps third party functionality behind an abstraction to facilitate interchangeability and ease of testing, even if the interfaces are defined much closer to the core.
  • Data – The Data relates to the physical persistence mechanism that is utilized on the behalf of the Domain.  Commonly, this includes the concrete implementations of the persistence-related interfaces inside of the Domain.  Those instances will be injected, where needed, keeping the persistence mechanism fully abstracted from the Domain, itself.  Common tactical patterns are usually implemented here, commonly seeing concepts surface related to Repositories and Context.  The Domain interfaces will guide the construction of the Data components, and any Domain consumption will be done solely through the interface references.
  • Application – The Application is a generic embodiment of the implementation of the actual application itself.  It not only contains the implementation, but also Application Services, which act as a bridge between the Application and the Domain and Infrastructure.  Commonly, the Application Services will build Commands and Queries for dispatch, consume Domain Services be host of raised Domain Events, all while talking through abstractions, to keep the highest level of the Application as lightweight as possible.

In practice, however, there is a bit more planning required.  I have seen both sample applications and full implementations which will break into those four divisions and stick firmly to them.  Again, as complexity grows, these solutions tend to bleed concerns and take on roles that would have been better suited being in a more finely-grained location.  The trick to all of it, ultimately, is finding a balance between granularity and ease of use.  If a Solution were to contain too many Projects, it becomes a maze of similarly grouped concepts which will become an exercise in frustration when trying to find specific code constructs.  Were a solution to be too coarsely grained, development becomes more akin to building monoliths over building houses.

Essentially, finding the correct level of granularity is a recurring theme, in that it needs to be metered to match the requirements of the enterprise and the skill-level of the team.  Before a single line of code is written, consideration needs to be given to non-functional aspects, such as branching strategy, resource allocation policies, deployment needs, and other nuanced details that will vary depending on the expectations of what the desired end-state of the project is.

One Size Fits Most

Again, the goal in determining the project structure is to try and account for what will meet the needs a majority of the time, and dealing with exceptional cases on a one-off basis, all while trying to maintain a maximum amount of value addition.  It is not simply about a place to put code, but rather looking at the blueprints from a full dev-ops perspective.  The decisions made will impact not just development, but testing, deployment, support, and the physical infrastructure.  Since Domain-Driven Design focuses on the team, instead of the individual, the input of anyone impacted should be taken into account.

To that end, and after trying to work with different levels of granularity, I have come up with a solution that generally tends to work for my own projects.  It is not geared towards providing a home for every single piece of code and related code artifacts to live, but rather providing a boilerplate that can be used to take the early aspects of laying the foundation into account without a dedicated ground-up evaluation for projects that have more in common than not.

The project structure that has become my boilerplate is:

  • {Root}.Core
  • {Root}.Domain
  • {Root}.Infrastructure
  • {Root}.Infrastructure.Data.{Implementation}
  • {Root}.Application
  • {Root}.Application.{Implementation}
  • {Root}.Application.{Implementation}.Bootstrap

Additionally, the boilerplate is a meta-boilerplate, as it defines the tests that I need.  Following the same convention, and where it makes sense, I will also have:

  • {Project}.UnitTests
  • {Project}.IntegrationTests

All of these projects deserve a little bit of explanation.  First, though, let me expand on the bracketed terms in the lists:

  • {Root} – The root project name is logically also the root namespace.  In all honesty, this is something that every shop will have its own preference on.  Personally, I find the root to be conditional.  Sometimes, it is a company name, while other times, it is a conceptual project name.    Then, there are times when it is both.  A lot of people shy away from putting conceptual project names as part of the namespace, because those can (and do) change.  Most all of my own projects are given “codenames”, which are different from how it will be branded.  It is good enough for mega-shops, and it is good enough for me, as well.
  • {Implementation} – The implementation indicates that there is a technology choice in play.  As it relates to {Root}.Infrastructure.Data, {Implementation} could mean NoSQL, or it could mean Entity Framework.  There could be multiple implementations, even on the same technology choice.  At that point, the implementation can be split to include descriptive modifiers, as well.  For {Root}.Application, the implementation could be as simple as Web, or specific like MVC.  Again, multiple implementations may exist.  For example, a conceptual project might include a Web implementation, along with a Desktop implementation, and a Mobile implementation.
  • {Project} – A project, in this context, is simply shorthand for a full project name in the first list.

Without further ado, some explanations are in order.

{Root}.Core

The Domain is the heart of the application, except for when it is not.  It is commonly said that the Domain should use the purest of constructs and should not depend on any outside dependencies.  That is a good and solid rule, but I have found it to be short-sighted in implementation.  Behind every good Domain is some level of framework.  While the Domain is an implementation of the Ubiquitous Language, the implementation is based on concepts that should be extracted.  These are the concepts that are highly reusable or extremely low-level.  It is at this level that the technical description of what these concepts are exists.  Here, we define interfaces, establish patterns, create generic abstractions, and essentially create our own extension to the .NET Framework which assist in facilitating the laying of the foundation.  If we are going to build a row of houses, all following the same basic blueprint, it makes a lot of sense to not have to find multiple ways to build that same foundation over and over again.  The Core will provide a “quick-start” for jump-starting a lot of the actual construction details, by taking the output and lessons learned from previous projects and bringing forward as much as we can to the next.

{Root}.Domain

Here, we have the actual Domain.  Unlike the hypothetical view of the overall architecture, the Domain is completely free from defining the patterns and interfaces that it needs to start construction.  If Core defines the interface of an IAggregateRoot or IEntity, it means that what defines those aspects have already been accounted for, and we can move straight into implementation.  It also hides, through abstraction, technical detail that a Domain Expert would not need to know about, should a developer walk one through the code.

It is also here that we deal with the Domain-Driven Design concepts of Modules.  In a C# world, these are usually represented through the use of strategic namespaces.  Unlike namespaces in more technically-oriented Projects, the Module representation through namespaces should express the Ubiquitous Language, instead.  Generally speaking, the Modules should become more granular, reading from left-to-right.  The first level of the Module may be the Domain, followed by a Subdomain, followed by a Bounded Context, followed by an Aggregate.  It will depend upon the complexity of the Domain, itself.  But, at all times, any technical terminology will be disallowed.  For example, we would want to avoid something like:

{Root}.Domain.Aggregates.Account.Events.CreatedEvent

Rather, we would prefer something like:

{Root}.Domain.Account.Created

To a Domain Expert, it reads easier and speaks the Ubiquitous Language.  There is no technical interference in the naming, and it conveys the important nouns and verbs of the Domain.  When read, there is no question as to what that Module expresses, through a concise namespace.  It is another small, yet recurring, theme.

{Root}.Infrastructure

Infrastructure concerns are generally technical in nature.  The Infrastructure project contains the technical implementations of discrete subsystems.  Again, leaning on the common example, an IEmailService could be considered a cross-cutting concern, whereas the Infrastructure would contain any one of a number of possible implementations of that concept.  Essentially, if there is a need to talk to a discrete subsystem, Infrastructure is where that happens; be it email, message queuing, persistence, or anything expressed through an external mechanism or protocol.  These generalized concepts are defined in our Core, for cross-systems awareness, but injected in at the appropriate levels.

{Root}.Infrastructure.Data.{Implementation}

The persistence part of the Infrastructure is treated like more of a first-class citizen than other Infrastructure concerns.  There are a couple of reasons to choose to do this, being both related to finding that correct level of granularity along with the fact that the persistence mechanism is likely going to be second, in importance, only to the Domain.  However, where a lot of examples will put the persistence into a generic Data package, I view persistence as nothing more than the Infrastructure concern that it ultimately is.  It is the part of the Infrastructure that will likely be used more than any other part (again, apart from the Domain), but that does not make it something other than what it is.

Here, we also have a second delineation of {Implementation}.  By default, the implementation designates a technology choice.  Often times, that is the only delineation that is actually needed.  However, if there are multiple data sources, it will become a means to more finely describe the specific source of the data, as well.  For example, if there are two databases that are used, there may be something like these two projects:

  • {Root}.Infrastructure.Data.EntityFramework.{First Source Name}
  • {Root}.Infrastructure.Data.EntityFramework.{SecondSourceName}

It makes sense, from a deployment aspect.  Assuming that there are multiple data sources, it is not unreasonable to assume that they could have different rates of change or complexity.  This level of granularity isolates the related changes that need to be made to the Context and any Repositories, along with other concrete implementations that are derived from Domain, such as Command Handlers, Query Handlers, and persistence mapping.  That is not to imply that every data source is the persistence mechanism of the Domain, either.  Should I want to surface an indexing service, like Lucene.NET, that would live as its own Data Implementation, even if it did not have the same Domain implementation requirements.

{Root}.Application

The Application is broken into multiple projects to facilitate reuse, and some forethought should be put into this structure.  This level of the application defines implementation-agnostic Application concerns, primarily through the expression of Application Services and Domain Event Handler implementations.  The role of these Application Services is to provide a common entry point to enforce conventions of accessing the Infrastructure and Domain, while the Domain Event Handlers will carry out broad implementation-agnostic tasks raised by the Domain.

Assume that there is a common functionality that is shared by any Application implementation, which requires the dispatch of a Command to a concrete Command Handler implementation, which lives in Infrastructure.  Building on the example from the last article, look at the creation of an Account.  The Application Service will express a set of requirements to create that Account.  To create that Account, the requirement is that we need a First Name, a Last Name, and an Email Address.  That data is passed into a CreateAccount() method in an Account-related Application Service, and that Application Service will construct a Command, with that data.  The Application Service will then validate that Command and, if valid, dispatch that Command to the Command Bus.  Assuming that the Command could be executed, the Command Handler will talk to the Domain to create the Account instance and the Account instance will queue an Account Created Domain Event for eventual dispatch.  Once the Account is created, the Command Handler will invoke the persistence mechanism, in the form of a Repository against the Context, then as the Account is persisted, its enqueued Domain Event is dispatched.  A Domain Event Handler, which also lives in the Application, is then invoked and may do something like send an activation email to the Email Address that was provided, by leveraging an Infrastructure service to construct and send that email.

Because the implementations within the projects will be loosely coupled, what sounds like a complex series of operations is actually going to be very straightforward to implement.  Coupled with injecting concrete implementations of these participants of the overall solution, there will be significantly less code to write to perform what would otherwise be complex operations.  Less code does not mean fewer classes, per se, but it will mean that throughout these projects, the implementations will be more concise.

{Root}.Application.{Implementation}

Much like there is an Application-agnostic series of implementations, there is a top-level project which dictates Application-specific implementation.  Unlike the broader Application concerns, the specific Application implementations are more product-specific.  If the product is an ASP.NET MVC web site, this could manifest as a {Root}.Application.Web project, for example.  Here, there is a strong emphasis on that implementation, and carries the responsibility of the UI and that specific technology choices that bring the UI together.  This is where the Controllers, View Models, and Views would live, and any supporting players in facilitating their usage.

Application Services can exist in an implementation, as well.  Taking the previous example, again, assume that there is both a web application and a mobile application, leveraging the same Infrastructure and Domain.  When creating an Account from the web application, we might want to collect additional information from the user, because the implementation allows for easier data entry than if the same user were to create an Account from the mobile application.  If the Application-agnostic Application Service had additional requirements, such as demographic information, a mobile-specific Application Service could be used for implementing a “lightweight” registration process, as long as it was supported by the Domain and met any business logic requirements.

{Root}.Application.{Implementation}.Bootstrap

Dependency Injection has been mentioned several times.  One of the key tenets of Dependency Injection is that a Composition Root be established to construct the dependency mappings as high up in the Application as possible.  Since the implementation is the top-level of a product, it stands to reason that each implementation has its own Composition Root.  Keep in mind that this does not mean that there are multiple Composition Roots, which violates best practices around Dependency Injection.  What it means is that each product has a Composition Root, expressed in its own Product-specific Bootstrap project.  Up until now, we have only referred to a single Solution.  If there are multiple implementations, however, it is a good practice to create a Solution per product, with each one providing a proverbial “view” of the Application that contains all product-agnostic concerns, plus the product-specific concerns, without including the implementations of other product-specific projects.  {Root}.Application.Web would contain every Project that is specific to its implementation, but nothing that was part of {Root}.Application.Mobile, for example.  This would help to isolate independent efforts and shield complexity from those who are working on specific implementations, while still promoting reuse.

One other thing that I like to do, is that if there are multiple product implementations, is that I will create a {Root}.Application.Bootstrap project, as well.  This allows for me to create the implementation-specific mappings in {Root}.Application.{Implementation}.Bootstrap, such as injecting Application Services into Controllers, but also to use the agnostic Bootstrap to extend the Composition Root for common mappings.  For example, assuming that all products use the same persistence mechanism, it makes sense that the construction of the specific Composition Root will build these dependencies identically, regardless of the Solution.  The only difference between the two is that the product-specific Bootstrap will consume the product-agnostic Bootstrap to build out the common dependencies.  This way, should an Infrastructure change take place that impacts the enterprise, that change needs to happen only once.  If we are using RabbitMQ as a message queuing mechanism for Domain Events inside of the Infrastructure and want to move to MSMQ, the Composition Root would only need to be changed once, at the product-agnostic level.

Putting It All Together

Here, we have discussed what the structure could be for a Solution.  Up until this point, I have very deliberately avoided using the term, “layers”.  We have a list of Projects, but still no explicit dependencies between them.  The reason for this is that “layers” have a connotation around them of being a rigid structure of top-down separations.  Approaching an application in this fashion tends to lead to very high cohesion and additional levels of abstraction that are unnecessary, at best.  In a traditional layered architecture, implementation and technological choices are often at the core of the Solution.  In Domain-Driven Design, though, the Domain is the core.  The implementation and technological choices are used to express the Domain, as opposed to defining it.  Dependency Injection goes a long way to address this, by allowing us to abstract those details from the Domain, and focus on what needs to be done, as opposed to how it needs to be done.

This is accomplished through a Hexagonal Architecture or Onion Architecture, which is more colloquial to the .NET community.  Both are extremely similar, with the primary differences between the two coming down, mostly, to terminology.  The key theme is that the Domain is the heart of any application, and that everything else exists solely to support the Domain.  For reasons already discussed, we do not consider our Core Project to be anything more than a framework, which reconciles this point.  In terms of Domain-Driven Design, both architectural approaches have largely become synonymous.  Considering that the Onion Architecture is the more commonly referenced .NET solution, we will focus on that.  As opposed to trying to reinvent the wheel, Jeffrey Palermo’s series of blog posts where he introduces the concept of the Onion Architecture is a great place to start.  He has a four-part series, which discusses the concepts (Part 1, Part 2, Part 3), along with a retrospective (Part 4).

Let us take a look at how we are specifically implementing the Onion Architecture, here:

A high-level view of the Onion Architecture.

A high-level view of the Onion Architecture, as it relates to our Project Structure.  The goal is to identify the relationships between high-level concepts, while pushing the Domain to the heart of the Application and externalizing dependencies.

The general concept is that the outermost layers have a knowledge of all of the layers within.  At the absolute outermost layer, we find our Composition Root.  This is telling us how the implementations inside of these layers have knowledge of each other.  No layer in the Onion has knowledge of a layer outside of itself, but it has knowledge of the layers within itself and may have knowledge of other concepts on the same layer.  The Application layer, for example, has no knowledge of how the Infrastructure is implemented.  Instead, the Application layer will have knowledge of an Infrastructure interface.  The concrete implementation will be injected into the Application, and the complexity of the implementation is then removed from the Application.  At the same time, the Application will have knowledge of the Domain while the Domain will have no knowledge of the Application.  Unlike where the Application is is told what concrete implementations to use from Infrastructure, there is no compelling reason to ever tell the Domain about any of its consumers.

One common goal is that the interfaces which make up the concrete implementations will either be at, or below, the actual implementations themselves.  This will be dictated by the needs of the application.  We could, at least within the constraints of the architecture, inject an Application Service into the Domain.  In reality, though, it makes no sense to do such a thing.  At the same time it does make sense to inject an Infrastructure implementation into the constructor a concrete Domain Event Bus, which would be closer to the Domain.  Those are dictated by the concepts expressed in Domain-Driven Design, itself.  As the project begins to take on more and more code, it will become useful to validate these relationships through Architecture Diagrams in Visual Studio.  They will provide a visual representation that these relationships are enforced, plus they can be used to instigate compilation failures, should they be violated.

That was a lot of ground to cover, but it is all going to be worth it, in the end.  In retrospect, we have established on possible solution to structuring a project on a hypothetical basis.  Next time, we will begin to actually build out the Solution and put these concepts in place.  It is going to take some time, and there will be some learning experiences along the way.  There will be course corrections, as we find better ways to accomplish our goals.  The most important aspect, though, is we will start putting this into practice and building upon it.  Hopefully, this means that we have a firm foundation to build our house on, and do not try to install windows or put the roof on before we put up the walls.