General project structure
Large software "monoliths" currently have a kind of bad reputation. Microservices are said to be the salvation from the unmanageable complexity that is the monolith. But what exactly are microservices doing better? They do have their advantages, for example:
- Independent scalability for different parts/services
- A crashing service crashes in isolation, leaving the others intact
- Ability to update parts independently
However, the main reason they are used is: They enforce interfaces between different services, meaning parts of the project. But they come with a number of disadvantages:
- Although the individual scope is smaller, you need more projects for the same work.
- You have to maintain network interfaces between your services.
- Moving a task over network multiple times incurs a higher latency for the user.
- The same amount of works needs more computational resources, sometimes significantly so.
- Since you operate a distributed system, you need advanced protocols for resistance agains parts failing and so on.
All in all, for the price of providing explicit boundaries, micro services introduce a significant amount of complexity, the very same thing that led to the bad reputation of monoliths in the first place. So in the common case, it is a no-solution.
So what to do instead? Think about your project structure!
A good project structure can grow organically, without sinking into chaos. Consider this: When any file grows too large, split it up into submodules. The original module then contains two kinds of code, common code to provide to the submodules, and interface code to tie them back together into one entity. The interface to other modules doesn't need to change at all. In fact, it also shouldn't. Don't directly link into anything other than the top module from outside!
With this, you have a two-way dependency: The submodules depend on the common code of their parent, while the interface code of the parent depends on the submodules. The interface code can also depend on the common code, but never the other way around, as that would create a cyclic dependency.
Please take note that different submodules of the same layer also should not directly depend on each other. If there is loose coupling, try to interface it through the higher level module tying them together. If there is tight coupling, it should not be in different submodules, anyway.
Code that is only loosely coupled to the main functionality of your project, such as generic collections, should be put into its own subtree and essentially treated like a seperate library. This code is also a prime candidate for moving into an actual separate crate.