Divide Your Codebase by Domains and Features To Keep It Scalable
Let’s make sure our code isn’t hard to navigate
Introduction
During my programming career, I had an opportunity to work on many different projects using different frameworks and different directory structures. My first experience was with simple MVC; later I saw MVVM and hexagonal architectures in action, and I’ve noticed a few things:
- although those architectures have some rules and concepts, different frameworks suggest different directories structures
- having a single directory for all models/controllers/view/<put any module type there> works well only in smaller applications. When the application grows, more and more issues occur, e.g., there are too many files in a single directory and it’s hard to navigate between them
- each programmer has their own preferences and if you don’t agree on a common set of rules (suggested by the framework or your own) for your directories’ structure you’ll end up with huge mess :)
The obvious conclusion was: your directory structure matters. It can make cooperation easier or be a reason for time loss and frustration when you’re trying to find something.
Picking the Best Approach for Your New Project
Usually, tutorials and frameworks docs suggest a simple directory structure, like one directory for all your views, another one for controllers, etc. In many cases, that’s enough! If you’re just learning a new technology or writing a private application — you don’t need anything else. Even if you’re working on a commercial app, but the whole implementation is planned for a few weeks/months and there are only 1–2 developers on the team — probably your application won’t grow so much and simple division by module/class type would be enough.
The fun begins if you’re starting a long-term project in your company — in that case, probably many different developers will work with that code (at the same time or not — it doesn’t matter). You would like to have a structure which:
- allows new developers to start working quickly, even if there are hundreds of thousands of lines of code
- is easy to maintain
- can be changed with time without much effort
Dividing Your Code by Domains/Features
The basic idea is to keep related modules/classes close to each other in your directory structure, so when your application grows you won’t be forced to search different directories to introduce changes in one functionality. Usually, changes in application are related to a specific domain or feature, you should focus on business domains when building your structure — not module/class types.
If you’re interested in implementing that approach in whole for your process, you can learn more about a Domain-Driven Design (DDD).
Let’s consider a simple application with directories by module/class types.
(I’m using the .js
extension to differ files from directories — you can apply the same principles to any programming language)
Not bad, maybe you’re used to it, but imagine that there’s more than 20 files in each directory. Now let’s structure it another way:
This kind of structure would allow you to change, e.g., companies logic without jumping between different directories even if there are many application modules. What’s more important is: if a new developer comes into a project and gets assigned to changes in the company’s logic, they would find all related files in one place!
That is a really simple example, and in such a case, both approaches would work fine. But usually features in your application are more complex and you need more than one class to handle their logic.
Following Division by Domains/Features of Your Logic To the Lowest Level
Let’s imagine that logic in thetask
directory is really complex and you’d like to divide a single controller into a few smaller modules/classes. That’s a situation when division by domains/features shines . You can introduce more and more files for specific features, and they will stick together. Let’s introduce some subdirectories related to specific features:
Building your application that way you should end up with multiple directories nested in each other. This might seem to be an issue, but remember that we’re talking about a structure suited for large applications with hundreds of modules/classes — so the alternative is having a relatively flat directory tree with dozens of files in each directory. There are reasons why a deep structure in this scenario is easier to maintain:
- it makes searching files related to specific features really easy. If you want to change something in “exporting tasks list to CSV,” you just open the next levels of tree: tasks > export > buildCsv.js. There might be more levels, but the rule would remain the same. That’s crucial for introducing new developers to the project, and it also makes reviewing code easier because all code related to feature is in one place, so you won’t miss anything.
- you can actually leverage nesting to hide low-level details — when you build your project structure that way, your directories become separate, self-sufficient modules — so you can expose the API for each of them and not care about internal details when using another’s module logic
Exposing API of Your Directories
Let’s talk more about using your directories as separate, self-sufficient modules. Consider the situation when you’re building the mentioned task
module and you need to use some part of it somewhere else (e.g., displaying all tasks assigned to the current user in user
module). Instead of referring to a specific file inside the task
directory, you can create a file exposing the “public API” of task
module and use it as follows:
That way you won’t need to deal with long imports paths and you’ll always know what parts of a specific module are used outside it!
What About Reusable Code?
One of the tough questions to be answered when dividing the structure by domains and features is where to put reusable code, which doesn’t belong to any domain (e.g., abstractions).
I guess there’s no one single solution that works for all cases. In general, you can try one of these:
- creating a dedicated directory that contains only reusable code, e.g.
abstractions
orcommon
- creating directories for abstract domains/features, e.g.,
list
for all abstractions related to lists,export
for all abstractions related to exporting, etc.
- mixing up both above — e.g.
abstractions
directory for pure abstract modules/classes and dedicated directories for other reusable code
You have to figure out what’s best for you and your project on your own. The good news is — you don’t need to worry about making a bad decision, as you can easily fix it later. For example, switching from the third example to the first one requires only moving cardPayment
directory into abstractions
and renaming abstractions
to common
. Most editors would automatically update all imports in your files when you’ll move them. The ease of refactoring is another reason why this structure is so great at scale!
Easy To Maintain, Hard To Create
Let’s summarize and talk about some pros and cons:
Pros:
- moving whole directories or renaming them is not an issue
- it’s easy to find all files related to a specific feature, which simplifies working with code you’ve never seen before (or haven’t seen for a long time)
- your code is split into multiple smaller modules, so you can apply big changes per module, one-by-one
- it’s easy to split your application into a few smaller parts if necessary (again, your code is already divided into self-sufficient modules!)
- when you’re building a new feature, similar to another one already existing, you can easily copy the whole directory and then start applying changes. Of course, copy-paste is an anti-pattern, but it might be useful when you need to create a new module similar structure. Please don’t duplicate business logic. That will always come back and bite you at some point
- not only your code but also your directories reflect an object-oriented design
Cons:
- it takes some effort to create a proper directory structure
- you have to update it when you’re adding/changing features
- finding a good place for reusable modules/classes might be harder than usual
- there might be some framework’s limitations like forcefully dividing part of your directory’s structure by type (e.g., some frameworks require keeping all models in a single directory)
In general, from my experience, you have to spend more time when building something new, but once it’s there, maintaining is much easier — and that’s something that works really well in long-term projects.
Some Final Tips
- try to not duplicate names in your directory path and file name (e.g. instead of
list/ListView
it’s better to havelist/View
). If you’ll need to rename the directory you won’t need to rename files inside it - follow division by domains/features to the very deepest level . When you’ll introduce a subdirectory-by-type (e.g.,
task/list/views
), you will lose whole flexibility of your structure here and if that subdirectory will grow it would require much effort to refactor it - always create a module/class exposing API of a specific directory. That’s a great way to make sure that your directory works as separate modules
(see the last example in Exposing API of your directories section) - don’t hesitate to experiment and mix some solutions from this article with your own — but make sure that rules for your structure are well-defined and accepted by the whole team
Thanks for reading; I hope you find it useful. If you have any questions, feel free to leave a response.