EXPEDIA GROUP TECHNOLOGY — ENGINEERING

Package by Feature, Not by Layer

How to organize source code to optimize dependencies

Fred Friis
Expedia Group Technology

--

Neatly packaged lunch
Neatly separated packages are important for maintainability (image credit: Anthony Shkraba, pexels.com)

Traditionally, most Java apps are organized by layer, which needlessly encourages large, unwieldy “God classes” and spaghetti dependencies across the system, where every class and package depends on every other.

Instead, consider packaging by feature.

Package by layer and what it leads to

How many times have you come across classes like this?

//...50+ importspublic class FooService {    private static final Logger LOGGER = LoggerFactory.getLogger(FooService.class);    private final FirstDependency firstDependency;
private final SecondDependency secondDependency;
private final ThirdDependency thirdDependency;
private final FourthDependency fourthDependency;
private final FifthDependency fifthDependency
private final SeventhDependency seventhDependency
private final EighthDependency eighthDependency;
private final NinthDependency ninthDependency;
public FooService(
final FirstDependency firstDependency,
final SecondDependency secondDependency,
final ThirdDependency thirdDependency,
final FourthDependency fourthDependency,
final FifthDependency fifthDependency,
final SeventhDependency seventhDependency,
final EighthDependency eighthDependency,
final NinthDependency ninthDependency
) {
//...
}
public Foo firstMethod(){
//...
}
//private methods to support firstMethod public List<Foo> secondMethod(){
//...
}
//private methods to support secondMethod public void thirdMethod(){
//...
}
//private methods to support thirdMethod //even more methods, who knows how many}

Where associated HTTP resource-level classes, underlying database layer classes, etc. all look similar.

Such classes are often the result of packaging by layer:

.
├── Main.java
├── domain
│ ├── Foo.java
│ ├── Bar.java
│ └── //more
├── db
│ ├── FooDao.java
│ ├── BarDao.java
│ └── //more
├── resources
│ ├── FooResource.java
│ ├── BarResource.java
│ └── //more
└── service
├── FooService.java
├── BarService.java
└── //more

Admittedly, it is tempting (and pretty intuitive) to split your code into controller, service, DB, and other kinds of packages; and then have one controller, service, or DB class for each “thing” in your system that deals with that domain.

While such organization and such classes superficially appear cohesive and self-contained (as they only deal with one domain), such organization often quickly leads to large, unwieldy packages and classes that have far too many jobs.

When packages and classes get too big, they become harder to understand, and harder to test. Their methods get more complicated in order to support different callers across the system that have slightly different needs. They get harder to work on as a team (e.g., merge conflicts resulting from different team members working on unrelated parts of the same package or class), and so on.

Package by feature and how it solves the problem

Consider the alternative:

.
├── Main.java
├── bar //no subpackaging needed
│ ├── Bar.java
│ ├── BarDao.java
│ ├── BarResource.java
│ ├── BarDao.java
│ └── BarService.java
└── foo //some subpackaging
├── create //but subpackaging by functionality, not layer
│ ├── PostFooResource.java
│ ├── CreateFooDao.java
│ └── CreateFooService.java
├── delete
│ ├── DeleteFooResource.java
│ ├── DeleteFooDao.java
│ └── DeleteFooService.java
├── get
│ ├── GetFooResource.java
│ ├── GetFooDao.java
│ └── GetFooService.java
└── Foo.java

The package structure is a bit more intricate, and there are more classes, but it’s still all intuitive. More importantly, now, packages and classes become much more cohesive as most of them have only one job. For example, instead of a single giant FooService, we get classes that look more like this:

//few or no importspublic class CreateFooService {    private static final Logger LOGGER = LoggerFactory.getLogger(FooService.class);    private final AuthService authService; //note the lack of countless dependencies
private final CreateFooDao createFooDao;
public FooService(
final AuthService authService,
final CreateFooDao createFooDao
) {
//...
}
public Either<Error, Foo> createFoo(
final User requestingUser,
final PostFooRequest request
){
if (authService.isAuthenticated(requestingUser)) {
return Either.right(createFooDao.create(request.toFoo()));
} else {
return Either.left(new NotAuthenticatedError(requestingUser))
}
}
}

Such classes are easy to understand, easy to test. Their methods remain simple because they don’t have to cater to countless callers with slightly different needs. And they’re easier to work on as a team (e.g., one team member can safely work in the get Foo package while another completely refactors the delete Foo package — there will still be no merge conflicts) and so on.

Note that not everything can be kept in feature packages — classes like AuthService, which inescapably will be used by services across the codebase, will still have to live outside feature packages. But such classes will be the exception rather than the rule.

“All our apps are already packaged by layer”

As is tradition! But consider at least trying packaging by feature in your next greenfield app and see if it makes any difference in the quality and ease of maintenance of the code.

Actually, even with pre-existing code bases that already use package by layer, there’s nothing preventing you from trying out package by feature — simply make a feature package for the next feature that’s implemented, or pick an existing feature and put it (or as much of it as possible) in a feature package.

It’s usually not a good idea (or even possible) to refactor a whole preexisting code base from one style to the other, but doing it little by little is perfectly valid and possible.

Learn more about technology at Expedia Group

--

--