Organizing your code might not seem an essential task at first. People don’t give it enough attention and the effects can become overwhelming when the project’s complexity grows.
Over the years, I have encountered many projects with various structures. At some point, things always became messy. I guess it had much to do with the people’s choices and mostly their ignorance. I want to believe that people don’t just throw things randomly in their homes as they do in code. And I’ve seen this a lot.
Think about having a class TransactionProcessor
which requires CustomerService
and BankAccountService
, both dependencies already being placed in the core-module
. Instead of segregating the existing code into customer-module
and bank-account-service-module
and adding them as dependencies to transaction-processor-module
and core-module
, some engineers will throw TransactionProcessor
inside the core-module
, because they can. By doing this, the TransactionProcessor
will also have access to services such as NotificationService
, which were also present in the core-module
module. Even worse, for some reason, we might end up with the BankAccountService
calling code from TransactionProcessor
because nobody forbids that. And then the second round of bad decisions is the code review phase, where people ignore the file paths and hit the approve button.
Another frequent example that shines among the bad things is that people propagate the DTOs that correspond to the web layer so deep that they even touch the data layers. Each layer has different needs, so you should end up with different DTOs per layer (of course, sometimes it makes sense to reuse them). If the code is organized in a well-fashioned manner, the above scenario could be easily avoided.
I’ve also seen cases where projects have an AccountDao
and a CustomerDao
and their roles are just mixing up by adding functionality to one that should belong to the other. For example, engineers might be tempted to add the AccountDao
function that fetches both accounts joined with customers. Why is this wrong?! Because customers depend on accounts, the function should’ve been added to the CustomerDao
in the first place. Why is this a big problem? Think about extracting the customer functionality to a separate service and you will inevitably touch code that affects the account.
Over the years, I have encountered many projects with various structures. At some point things always became messy. I guess it had much to do with the people’s choices and mostly their ignorance. I want to believe that people don’t just throw things randomly in their houses.
Nowadays, everybody goes for microservices, even when they don’t need to. Many companies have already broken their monoliths into smaller chunks, and one of the biggest struggles they encountered was the actual code extraction. This step could’ve happened in hours instead of weeks with a well-organized code.
The solutions I will discuss next proved very efficient in many complex projects. They rely on domain models. To keep it short, domain models should make you think in terms of business models and their relationships rather than technicalities. If this still seems unclear or you haven’t heard about DDD (Domain-Driven-Design), go out there and learn more about it. It will prove helpful in many other cases too. Maybe start with this one.
The following code snippet has three domain models: user
, article
, and newsletter
, each containing only its related functionality, living as independently as possible. It can be implemented easily as long as the project is able to use directory structure (that means almost everywhere).
app -- src ---- user ------ User ------ UserRepository ------ UserService ------ UserResource ---- article ------ Article ------ ArticleRepository ------ ArticleService ------ ArticleResource ---- newsletter ------ Newsletter ------ NewsletterJob
The above solution has some advantages:
- it keeps the whole functionality related to a business domain in a single place (package/folder)
- allows easy extraction of functionalities to individual modules or to separate services – that would basically mean a copy-paste
- searching for a specific functionality is much easier since everything is organized around business domains
In a project that uses a build tool such as Maven or Gradle, you can go one step further and organize your code into modules.
user-module -- src ---- User ---- UserRepository ---- UserService ---- UserResource article-module (depends on user-module) -- src ---- Article ---- ArticleRepository ---- ArticleService ---- ArticleResource newsletter-module (depends on user-module and article-module) -- src ---- Newsletter ---- NewsletterJob
Having separate modules comes with another set of benefits:
- dependencies are always known and easy to identify
- modules are isolated from each other
- the complexity of each module is reduced
- can speed up automated testing by using parallel execution
Another approach is to segregate the web layer from the other layers. There are multiple ways of doing this and the following one has a single web layer for the entire application. This solution works well if you don’t have microfrontents and will help avoid the problem I described in the beginning, which referred to passing the same DTOs through all layers. Think about the article-module
, which uses the user-module
. Why would ever article-module
need the UserResource
? The article module should contain only the business logic related to the article and shall not have anything to do with the web layer.
user-module -- src ---- User ---- UserRepository ---- UserService article-module (depends on user-module) -- src ---- Article ---- ArticleRepository ---- ArticleService newsletter-module (depends on user-module and article-module) -- src ---- Newsletter ---- NewsletterJob web-module (depends on user-module and article-module) ---- UserResource ---- ArticleResource
The only disadvantage I can think of when it comes to the solutions above is that you will need to invest a bit more time and effort to keep things neat.
These solutions promote simplicity and extensibility. One can easily adapt those structures and transform them into something more relevant and specific to the project.
Conclusion
I did quite many functionality migrations from monoliths to microservices. Because of the poor code structuring, I ended up spending way too many unnecessary additional hours to achieve the goals. All these problems would’ve been solved if people had paid more attention to where they added the code.
At some point, all strategies for organizing could (and will) end up messy as long as the project evolves and people come and go. Somehow it’s inevitable since we are all different, think differently and do things in various ways. People need to be willing to do things better and to educate themselves to maintain a high level of discipline.
The proposed solutions are not a recipe that can fit all use cases. But if you need a better structure for your project, give them a try. The projects tend to evolve in unforeseen ways, so this solution could save y