Software application interior design
“Space, light and order. Those are the things that men need just as much as they need bread or a place to sleep.”
Le Corbusier
At the beginning of a project, the question always comes up: “How do we structure the project?” Should we follow the default package structuring pattern offered by the framework or choose anything else? I need to point out that I use the term “package” because, from my point of view, it is a clearer term to describe an independent part of a program that can be easily moved from one place to another, taken out for reuse or deleted altogether.
To understand the task scope, we need to outline two extremes. On the one hand, we have an option with no structure at all, as if we are trying to write the whole program in one file; on the other hand, we have an overcomplicated, deeply nested structure with each class in a separate package. As you can guess, the best possible option is somewhere in the middle. But what does it look like and what principles should we stick to when managing the project? Keep reading!
Turnkey apartment
Most frameworks like ASP.NET, Spring, Ruby on Rails, or Django offer the Model-View-Controller MVC [1] pattern as a standard for project organization. But why do they offer this approach? Is it the best option and in what cases? We need to go back to the origins to answer these questions and to understand why this pattern is used.
The MVC pattern was first described by Trygve Reenskaug in 1978. It is easy to understand. You can use it to solve the following tasks due to the layered architecture:
- code splitting,
- code reuse,
- creating different representations for the same model.
This pattern is suitable for client-server applications, although it was originally designed for desktop applications. Since then, other patterns have been added to the category of structural patterns, such as:
- Presentation–abstraction–control (PAC) 1987
- Model-View-Presenter (MVP) 1990
- Entity-Boundary-Interactor (EBI) 1992
- Presentation Model 2004
- Model-View-ViewModel (MVVM) 2005
- Resource-Method-Representation (RMR) 2008
- Models-Operations-Views-Events (MOVE) 2012
- Action-Domain-Responder (ADR) 2014
Some patterns are more suitable for client-server applications, because they better match the HTTP request/response cycle, while others are less suitable for this. If you look at the timeline, you may notice that the “first mover advantage” is fundamental to making the MVC approach appear in most frameworks as the default pattern. Now, let’s try to answer the question: is this really the best option for structuring a project?
In small projects, package organization is not critical, but all successful projects tend to grow. However, in projects with a large codebase, especially if you’re creating a monolithic application, it makes a big difference. Developers are frequently tasked with finding the code snippet responsible for a certain function. Since at the start of the project the default pattern is usually chosen, let’s take a look at an example of such a project at the initial stage.
At the first nesting level, we already have at least three packages. It will likely be necessary to create the Repositories package for the data access layer. You will also need the Services package for reusing common parts of the user’s business scripts. As a result, we end up with the following structure:
MyCompanyNameProject
- models
- views
- controllers
- services
- repositories
It has become popular recently to create web applications using the web service + REST API + SPA client bundle. This means that the backend is not responsible for the representation of our resulting application, but only provides a tool to retrieve and modify data, for example using the REST API. Therefore, the Views package is deleted and replaced by the Dtos package with Data Transfer Objects — classes describing the REST API method contract. Different frameworks may use different terminology, and the term “entities” may be used instead of “models” and “resources” — instead of “controllers” in some cases.
Now, imagine that you have just joined a project, and your task is to expand the existing functionality. For example, add user notifications. To do this, you need to localize the part of the program that is responsible for it and figure out what is already done and what needs to be added. Looking at the structure from the example above, we can’t say anything about the features of our program and in which package the part of the code responsible for notifying users is located. We can only use global search and intuition or go from package to package and read the names of nested packages and classes. Also, if the project becomes large, there will probably be a couple of dozen packages at the first nesting level, and each package will have dozens or maybe even hundreds of classes.
In simple cases, this folder structure works well, but as the project grows, navigating and understanding it becomes problematic. This approach is suggested only because the framework cannot anticipate specific features of your project and offer you a more sophisticated structure. This responsibility lies entirely with the developers.
The arm’s length principle
Imagine that you have bought an apartment, and you are free to plan its room layout. When you are planning your apartment furnishings, you easily find a place for the dishwasher, sink and dish dryer. The building’s architect has already thought this through and ran pipes to the part of the apartment where the kitchen is supposed to be. In this case, we follow the rule: the sink, dryer, and other things are used in the same context.
When structuring a project, it is also better to stick to contextual coherence rather than linguistic similarity. Surely, you don’t want to put a washing machine and a sewing machine next to each other. For the same reason, you should not put all controllers in the same folder, just because the name of the classes ends with the same word.
Another factor that matters is covering similar user scenarios with an individual package without the need to use other packages. In our example, the kitchen is a separate package. The kitchen is responsible for several user scenarios, such as:
- cooking,
- dishwashing,
- food storage.
They are all closely related and require the use of items that are similar in purpose. We need a spoon for cooking and eating, and we have to wash it afterwards and keep it somewhere handy. Sometimes, we may need a chair from the study if we have more guests than we expected, but that doesn’t mean we should combine the study and the kitchen. The same applies to development.
We also have plumbing, wiring and heating in the house — these are the most important things if you want to live in comfort. These utilities are designed first, the work on them is thoroughly planned, as they run through the entire house and changing these systems in the future after construction is completed results in certain difficulties and costs. In the house, we rarely move a tap or an outlet or add a new one. In programming, on the other hand, it happens constantly during every new sprint. This is called a cross-cutting concern. Often, you can implement it using an individual package and add it to other packages by means of the aspect-oriented programming. You can decorate methods and classes to add cross-cutting concerns very quickly without messing up the logic of the underlying code by using a declarative approach.
DDD interior
First, let’s formulate the key principles of a well-structured project:
- The functionality you need is easy to find in it.
- Packages of the same level are consistent with each other.
- Packages are loosely coupled to each other.
- There is high cohesion inside the package.
There are several concepts in DDD that directly affect the project structure. Bounded contexts are among these concepts. They reflect the global division of the product into separate, highly autonomous subsystems. It would be reasonable to reflect this at the first nesting level. For example, in our apartment project, it could be kitchen, bedroom, living room, bathroom, and so on.
At the second level of nesting, the division is related to the technical aspect of program development. Inspired by DDD, we would like to have a pure kernel with encapsulated business logic in it. So, let’s allocate a separate “domain” package for this. We will use it to collect all our aggregates, their events, business services, and interface definitions for accessing and storing aggregates. In addition, we always need a layer through which our users can interact with the domain, while ensuring transactionality, competitive access and authorization rules. Let’s add the “application” package for this purpose. The main purpose of this package is to coordinate the interaction of infrastructure items and delegate the execution of requests to the domain. These APIs can use various transport communication protocols to receive requests from the outside world. We run into something similar when implementing a data access layer. We’ll put all the technical stuff into a separate “infrastructure” package.
Let’s dive deeper and try to understand what is inside each previously defined package. At the third level of nesting in the “domain” package, there should be packages with names clear even to non-programmers. These packages often reflect the division of the domain into features. For this, we will need to use ubiquitous language terms. There is a list of rules for package design in Vaughn Vernon’s book, but packages are called modules there.
As an example, let’s use software development as a service.
The basic rule when diving in is that names change from more general to more specific. Going through each level, we see that the packages are consistent with each other. The first level reflects the global division of the product, the second — technical division in order to emphasize the domain, the third — division by features. As a result, you can select what you need easier and faster. If you need to reuse, move or delete a package, as little effort as possible will be required, since this structure ensures low coupling between packages and high cohesion within them.
It is also worth keeping an eye on the dependencies between packages. If packages are properly organized, the domain package should not depend on other packages. The dependencies between the packages within a single bounded context are as follows.
Moving in
At the end, I would like to point out that programmers develop programs for other programmers. And the processor does not care what our packages, classes and variables are called. That is why we should rely more on psychology and human perception, rather than on the popularity of a particular pattern for architecture structuring. We can structure a project in order to optimize the time needed for the programmer’s daily tasks. This approach has a number of advantages, which we have described, but it also has disadvantages. As we know, naming is one of the hardest programming tasks. In practice, this can be solved by applying Event Storming to jointly develop a ubiquitous language and a domain area model.
By Michael Lazko, Senior Backend Developer at QuantumSoft
- Martin Fowler — GUI Architectures
- http://www.rusdoc.ru/articles/18358/
- https://docs.microsoft.com/ru-ru/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice
- http://aspiringcraftsman.com/2007/08/25/interactive-application-architecture/
- Vaughn Vernon Implementing-Domain-Driven-Design