The package is a fundamental concept in Java — and one of the first things you stumble upon when starting programming in the language. As a beginner, you probably don’t pay much attention to the structure of packages, but as you become a more experienced and mature software developer, you start to think what can be done to improve their efficiency. There are a few major options to consider and picking the right one might not be an obvious choice. This article should give you an overview of commonly selected strategies.
Why Do We Use Packages?
The second thing is that packages allow defining access modifiers for particular members of a project. Accessibility of a class, an interface, or one of their members like fields and methods can be limited or completely prohibited for members of different packages.
Both features are, first of all, used by the compiler to enforce language rules. For clean coders and software crafters, the primary property of a package is the possibility to have a meaningful name that describes its purpose and the reason for existence. For compilers, it’s just a random string of chars, while for us, it’s another way to express our intention.
What Is a Package?
In the official Java tutorial, we can find the definition, which begins like this:
A package is a namespace that organizes a set of related classes and interfaces. Conceptually you can think of packages as being similar to different folders on your computer. You might keep HTML pages in one folder, images in another, and scripts or applications in yet another. (…)
The first sentence emphasizes the organizational purpose of packages. The definition doesn’t explain, though, what kind of a relationship classes and interfaces should have to consider them as a single group. The question is open to all software developers. Recently, Kent Beck wrote a general piece of advice that also applies to the topic discussed in this post:
If you're ever stuck wondering what to clean up, move similar elements closer together and move different elements further apart
Yet, just like the word “related” in the aforementioned package definition, the word “similar” can have completely different meaning to different people. In the remaining part of the article, we will consider possible options in the context of package organization.
Package by Layer
Probably the most commonly recognized similarity between project classes is their responsibility. The approach that uses this property for organization is known as package by layer or horizontal slice and, in practice, looks more or less like on the picture below.
If you didn’t have a chance to work on a project with such a structure, you could come across it in some framework tutorials. For instance, Play Framework recommended such an approach up to version 2.2. The tutorial of Angular.js initially suggested keeping things together based on their responsibilities. The fact they changed their opinion about the subject and updated tutorials should probably excite you to think what the reason was. But before we judge the solution, let’s look at its strengths and weaknesses.
Considering layered architecture as the most widely used, it shouldn’t be surprising that developers aim to reflect the chosen architecture in the package structure. The long prevalence of the approach influences the decision to apply the structure in new projects because it’s easier for a team’s newcomers to adapt to an environment that is familiar to them.
Finding the right place for a new class in such an application is actually a no-brainer operation. The structure is created at the beginning of development and kept untouched during the entire project’s existence. The simplicity allows keeping the project in order, even by less-experienced developers, as the structure is easily understandable.
Some people say having all model classes in one place makes them easier to reuse because they can simply be copied with the whole package to another project. But is it really the case? The shape of a particular domain model is usually valid only in a bounded context within a project. For instance, the product class will have different properties in a shopping application than in an application that manages orders from manufacturers. And even if you would like to use the same classes, it would be reasonable to extract them to a separate JAR marked as a dependency in each application. Duplication of code leads to bugs, even if it exists in separate-but-related projects, hence we should avoid copy-pasting.
A major disadvantage of the package by layer approach is overuse of the public access modifier. Modern IDEs create classes and methods with the public modifier by default without forcing a developer to consider a better-fitting option. In fact, in the layered package organization, there is no other choice. Exposing a repository just to a single service class requires the repository to be public. As a side effect, the repository is accessible to all other classes in the project, even from layers that shouldn’t directly communicate with it. Such an approach encourages creating unmaintainable spaghetti code and results in high coupling between packages.
While jumping between connected classes in IDEs is nowadays rather simple no matter where they are located, adding a new set of classes for a new feature required more attention. It is also harder to assess the complexity of a feature just by looking at code as classes are spread across multiple directories.
At the beginning, we said that the name of a package should provide additional details about its content. In the package by layer approach, all packages describe the architecture of the solution, but, separately, they don’t give any useful information. Actually, in many cases, they duplicate information present in class names of its members.
Package by Feature
On the other side of the coin, you can structure your classes around features or domain models. You might have heard of this approach as the vertical slice organization. If you work only with the horizontal slice, at the first glance, it might look a little bit messy. But in the end, it’s just a question of mindset. The following picture represents the same classes as in the previous paragraph, but with a different package layout.
You probably don’t keep all your left shoes in one place and all right in another just because they fit the same feet. You keep your shoes in pairs because that’s the way you use them. By the same token, you can look at the classes in your project. The core idea of the vertical slice is to place all classes that build a particular feature in a single package. By following this rule, in return, you will receive some benefits but also face some negative consequences.
When all feature classes are in a single package, the public access modifier is much more expressive, as it allows describing what part of a feature should be accessible by other parts of the application. Within a package, you should favor usage of the package-private modifier to improve modularization. It’s a good idea to modify default templates in your IDE to avoid creating public classes and methods. Making something public should be a conscious decision. Fewer connections between classes from different packages will lead to cleaner and a more maintainable code base.
In the horizontal slice, packages have the same set of names in each project while, in the vertical slice approach, packages have much more meaningful names describing their functional purpose. Just by looking at the project structure, you can probably guess what users can do with the application. The approach also expresses hierarchical connections between features. Aggregate roots of the domain can be easily identified, as they exist at the lowest level of the package tree. The package structure documents the application.
Grouping classes based on features results in smaller and easier-to-navigate packages. In the horizontal approach, each new feature increases the total number of classes in layer packages and makes them harder to browse. Finding interesting elements in the long list of classes becomes an inefficient activity. By contrast, a package focused on a feature grows only if that feature is extended. A new feature receives its own package in an appropriate node of the tree.
It’s also worth mentioning the flexibility of the vertical slice packaging. With the growing popularity of the microservice architecture, having a monolith application that is already sliced by features is definitely much easier to convert into separate services than a project that organizes classes by layers. Adopting the package by feature approach prepares your application for scalable growth.
Along with the development of the project, the structure of packages requires more care. It’s important to understand that the package tree evolves over time as the application gets more complex. From time to time, you will have to stop for a while and consider moving a package to a different node or to split it into smaller ones. The clarity of the structure doesn’t come for free. The team is responsible for keeping it a good shape with alignment to knowledge about the domain.
Understanding the domain is the key element of clean project structure. Choosing a right place for a new feature may be problematic, especially for team newcomers, as it requires knowledge about the business behind your application. Some people may consider this as an advantage, as the approach encourages sharing knowledge among team members. The introduction of a new developer to the project is slightly more time consuming, yet it might be seen as an investment.
You may think that no extremity is good. Can’t we just take what is best from both approaches and create a new quality that is intermediate between two extremes? There are two possible combinations. Either the first level of packages is divided by layer and features are their children, or features build the top level and layers are their sub-nodes.
The first option is something that you might have encountered, as it is a common solution for packages that grow to large sizes. It increases the clarity of the structure, but unfortunately, all disadvantages of the package by layer approach apply just like before. The greatest problem is that the public modifier must still be used almost everywhere in order to connect your classes and interfaces.
With the other option, we should ask the question of whether the solution really makes much sense. The package by feature approach supports organization of layers, but it does it on the class level and not using packages. By introducing additional levels of packages, we lose the ability to leverage default access modifiers. We also don’t gain much simplicity in the structure. If a feature package grows to an unmanageable size, it’s probably better to extract a sub-feature.
Picking the package structure is one of the first choices you have to make when starting a new project. The decision has an impact on the future maintainability of the whole solution. Although, in theory, you can change the approach at any point in time, usually the overall cost of such a shift simply prevents it from happening. That is why it’s particularly important to spend a few minutes with your whole team at the beginning and compare possible options. Once you make a choice, all you have to do is to make sure that every developer follows the same strategy. It might be harder for the package by feature approach, especially if done for the first time, but the list of benefits is definitely worth the effort.
If you have any experience with the vertical slice and would like to add your two cents to the topic, don’t hesitate to share your thoughts in the comments. Also, please consider sharing the post with your colleagues. It would be great to read the outcome of your discussions and feelings about the approach.