Structuring packages in Java web applications

Photo by Rubén Bagüés

Indrek Ots
by Indrek Ots
7 min read

Categories

  • articles

Tags

  • java
  • package
  • architecture
  • layer
  • spring

If you’ve ever worked on a typical Spring web application, most likely you have seen the following top level packages.

.
├── controller
├── service
└── repository

This clearly indicates that a layered architecture is used. We have a package for controllers that accept incoming HTTP requests. service package includes classes that deal with application logic and finally, repository contains data access functionality.

Classes are grouped like cutlery on a tray. Image by Jarosław Ceborski

Classes are grouped like cutlery on a tray. Controllers, services and repositories are in separate packages the same way knives, forks and spoons are in their respective containers. It’s a widely used approach that’s familiar to many software developers. But have you ever questioned why do we build applications this way? Why do we treat layers with such importance that they get a package named after them? Perhaps this is a cargo cult? In this post we’re going to explore the ideas around what would happen if layers were not the topmost level of organization in an application architecture.

Quick recap of packages

In Java, packages can be used for the following

  1. Organizing classes into logical groups
  2. Namespace classes to avoiding naming conflicts
  3. Encapsulation

In my opinion, using packages as a tool to achieve encapsulation is the least used function of the three. Packages are more than just folders on disk that organize classes the way you might organize your photo collection. They can be used to define boundaries in code and hide classes from other parts of the system that don’t need to know about them.

Package by layer

The first chapter of Patterns of Enterprise Application Architecture by Martin Fowler says that layering is one of the most common techniques that software designers use to break apart a complicated software system. They make an intricate system easier to reason about. In Java, we usually see layers implemented using packages. If you have ever worked on a Java based web application, you have probably seen it yourself.

Layered architecture modelled with packages.
Layered architecture modelled with packages. Types in each layer have to be public.

Designing packages by layer is technical in nature and does not reflect the underlying domain. Whenever a layer above calls a layer below, we’re crossing package boundaries. To do that, the callee needs to be public. For example, if a service class inside the service package wants to call a method in a repository class that’s in the repository package, the latter needs to be public.

What’s bad about having a type declared as public you might ask? Having it publicly accessible could be beneficial, because there might be other service layer classes that want to operate on the same repository, right? Before giving an answer, I’d like to take a step back and talk about encapsulation.

Encapsulation

When we design a class, we need to consider what parts of it to hide and what to make public. Essentially, we’re using access modifiers to hide the inner workings of a class from the outside world. We don’t want anybody to freely access the private parts of our class because (a) we want to hide complexity and (b) have the freedom the change implementation details without having the entire world know about it. Using packages for encapsulation, it is possible to achieve the same goals but on a higher level of abstraction.

Unless we’re dealing with a demo application, most of the time, accessing a repository can be considered an implementation detail of a larger feature. In addition to, say, storing data to a database, you might need to introduce logging, check permissions or start other business specific processes. For example, whenever a new user is created in your SaaS product, you might want to send them a welcome e-mail. If a hypothetical UserRepository was publicly accessible to all other classes, it’s easy to bypass important domain related checks and processes without even knowing that you might have done something wrong.

Everything public

Designing packages by layer means that the classes that are used together the most are in different packages. This leads to low cohesion. Our classes need to be public, otherwise the layer above cannot call them. When you think about it, we’ve hardwired ourselves to always use the public access modifier when creating a new class. It does not help that public is usually the default option when generating a new class via an IDE. And when something is public, every other class can call it. Without discipline, it’s easy to introduce architecture violations by declaring a dependency on a class that might just be an implementation detail. There’s a higher risk that the system evolves into something that’s difficult to change. Using a package-by-layer approach, we intentionally give away the benefits of encapsulation.

If all types are public, Java packages are about organisation of code rather than encapsulation

Package by feature

If we stop doing package by layer, how should we structure our code? Instead of creating a package for each layer (a horizontal slice), what if the topmost level of organization in code was a feature (a vertical slice)?

Packages representing a domain concept
Slicing software vertically with domain specific packages. Layered architecture is still present but hidden from the top level view.

Compared to the package-by-layer approach, classes that are used together the most are now in the same package, allowing us to design more cohesive modules. There’s no benefit in making all classes public anymore. As a matter of fact, we gain more from starting to create package-private classes. Types that are specific to a feature can be made package-private so they don’t leak outside of their domain. On the other hand, public classes define the API of the package their part of.

When slicing software vertically, we have better control over how classes call each other over package boundaries. For example, a repository class could be made package-private so that nobody from any other package could call it directly. Access to a repository can be encapsulated and callers can be forced to go through the public API. Therefore, implementation details of a feature are hidden from the outside world. Layered architecture could still exist inside a package but layering in general becomes an implementation detail.

So what does the architecture of your application scream? When you look at the top level directory structure, and the source files in the highest level package; do they scream: Health Care System, or Accounting System, or Inventory Management System? Or do they scream: Rails, or Spring/Hibernate, or ASP?

Packages could be designed around Domain Driven Design aggregates. When it makes sense, vertical slices can become bounded contexts. If you see a need, they could be extracted into a separate Maven module. With the advent of the Java Module System, it is possible to think about domain boundaries at a higher level of abstraction. Theoretically it should be relatively painless to extract a vertical slice into a separate application when we have well defined APIs in place between modules.

In Release It!, the author Michael T. Nygard writes about the benefits of vertical slices.

What happens if we rotate the barriers [horizontal slices] 90 degrees? We get something like component-based architecture. Instead of worrying about how to isolate the domain layer from the database, we isolate components from each other. Components are only allowed narrow, formal interfaces between each other. If you squint, they look like microservice instances that happen to run in the same process.

Summary

Abstraction helps us to reason about big and complex software systems. In Java web applications, we usually see that the first level of abstraction are layers implemented with packages. As the application grows, layers become more complex and further modularization might be needed. Creating a package for each layer might not be the best approach anymore because we lose the benefits of encapsulation that packages can provide. Package-by-layer approach forces us to create classes that are public. As a result it’s easy to introduce architecture violations because every part of the system can access public classes.

Instead of decomposing software systems along technical lines, what if we decomposed software into domain specific modules? Creating packages by feature allows us to hide classes from other parts of the system that don’t need to know about them. We’re benefitting from encapsulation and can enforce how modules should talk to each other.

Coming up with module boundaries is a difficult task where one might have to redraw the boundaries multiple times before settling on a solution that feels right. Layered architecture using packages is usually a simpler starting point and perhaps that’s one of the reasons we see it more. Packages are also not the perfect tool to design modules. Hopefully the Java module system can help us here.

Where do I start?

Stop making every class public

This website is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to Amazon.com.