In this post I will give you some simple advice that, when followed carefully, will immediately lead to an easy to maintain and scalable architecture of your Java application. While this post is dedicated to Java the concepts will equally apply to other languages that allow to arrange code in packages.
One could write books of how to achieve good architecture (and people did) but you need a better architecture right now and are short of time - and so am I - so let's jump into it. Everything I'm going to tell you now boils down to this single most important advice:
Use package private scope liberally
If you do, this solves so many problems at once that, once you got used to it, you will hopefully be as amazed as I was when I realized how valuable this advice is. Let me explain this in detail.
In bigger applications the key to maintainability is to keep coupling of the
application's single components under control. This assumes that your application must actually be structured into components in the first place. If it is not, you probably alreay experienced some kind of inter-package-dependency-hell with package cycles and the alike (the good ol' big ball of mud).
Dependency is the key problem in software development at all scales (Kent Beck)
Java provides the concept of packages to structure your code into components. But experience taught me that most times, a single component is spread across multiple packages. This happens when programmers choose their packages under technical aspects. For example if you have a package
org.domain.app.controllers where all your controllers live and another package
org.domain.app.services where all your services live you have a technical package structure. In this scenario, possibly unrelated classes are located near to each other in the same package while related classes are located far away.
Locality of related code is a key aspect of having code that is easy to reason about
Because your related classes, which together solve a certain domain specific problem are located in different packages, they all have to be public in order to work together. By choosing to not place related classes into the same package you open yourself up to all the evil of having certain classes used in places where you don't want them to be used.
If upfront you chose a domain or feature related package structure there were an easy soultion to avoid this problem in the first place:
Give as many as possible classes the package private scope
Actually you should, by default, make every class package private and only broaden its scope if necessary. After some time you will develop a bad feeling whenever you see something beeing public - and this is a good thing. It makes you think explicitly about the public interface of your package. Following this advice you will automatically create packages with following characteristics:
- high cohesion: everything in the package is kind of related to the other things. The locality of related code will make it easy to reason about.
- low coupling to other packages: because there is only a minimal public interface of your package
While these are good things you will also experience that your packages grow bigger and bigger up to a size that make you feel uncomfortable.
If you feel your package is growing too big that is a clear sign that there might be a better domain specific cut. In this situation you should review and question the whole purpose of the current package. In my experience you run into the following cases:
- Your original feature got undermined by unrelated code which actually serves a different feature.
- As time passed, the requirements for the original feature changed that much that the original structure does not fit sufficiently anymore. It might also well be possible that the feature did not change, but your understanding of it did while you were coding it.
Most times the first case is simple to solve. If you are really in a situation where mostly unrelated code slipped into your feature package it should be pretty easy to isolate it and move it into its own package.
If isolating smaller parts is not that easy you might be subject to the second case. This is where the sorcery of package private reached its limits and you will have to do actual work. You are now up to some refactoring on package level - that is you need to split your current package into multiple new ones which better suit the requirements of the domain problems you are trying to solve. This is most likely not solvable by only moving some classes between packages but might require actual code changes.
But because everything related is so near together and you know that most things are not used outside the package this gives you great confidence while refactoring.
Have you ever wondered what the unit in unit testing is? You might have come across test suites where a test class exists for every production class. If so you most likely experienced that your unit tests are not very robust. They break easily if you adjust the implementation class even if you actually preserved behavior correctly. That is because unit does not mean class. Unit pertains to all the code you want to test in isolation in order to solve a domain specific problem. Unit means feature and feature, in our case, means the public interface of your package.
Don't write a test class for every production class. Write a test class for every feature
Doing so loosely couples your test code to the production code in the same way your production code is loosely coupled to other packages. You can freely change all the implementation details (like, even delete classes) behind the public interface without breaking your tests. That is the very meaning of information hiding.
To summarize, by using package private scope liberally you get (for free!)
- high cohesion within your packages
- low coupling between packages
- a durable architecture because of well defined interfaces between components/packages
- code that is easier to reason about
- a good hint of what you should test in order to achieve more robust unit tests
And again: you get this for simply leaving out the
public modifier in you class declarations! Whenever you feel the need to create a new class, make it package private and place it into the package where, from a domain or feature point of view, you think it fits best. This puts you in a position where you can evolve your package layout carefully over time.
The attentive reader might have noticed the following: the above tips on structuring your packages is actually the same advice we followed along for decades on class level. It is undisputed that it is best practice to keep helper methods private in order to have a well defined public interface. If "dependency is a problem at all scales" it is likely and expectable that the solutions for managing dependency look similar at all scales too.
- Simon Brown on 'package by component': http://www.codingthearchitecture.com/2015/03/08/package_by_component_and_architecturally_aligned_testing.html
- https://github.com/olivergierke/moduliths (A spring Boot extension which tries to enforce this way of structring your application)
- Somthing about to DDD