Skip to content

Instantly share code, notes, and snippets.

@androidfred
Created September 15, 2021 00:40
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save androidfred/787d4c11a3e3cf544bde9a623b95bd90 to your computer and use it in GitHub Desktop.
Save androidfred/787d4c11a3e3cf544bde9a623b95bd90 to your computer and use it in GitHub Desktop.
Package by feature - not by layer

Package by feature - not by layer

TLDR, summary

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

Instead, consider packaging by feature.

Longer version

Package by layer and what it leads to

How many times have you come across classes like this

//...50+ imports

public 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 db layer classes etc etc all look similar as well.

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 etc packages, and then have one controller, service, db etc class each 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, harder to test, their methods get more complicated in order to support different callers across the system that has slightly different needs, they get harder to work on as a team (eg merge conflicts result from different team members working on unrelated parts of the same package or class) etc etc.

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 becomes much more cohesive as most of them have only one job. Eg, instead of a single giant FooService, we get classes that look more like this:

//few or no imports

public 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, they're easier to work on as a team (eg, 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) etc etc.

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 codebases 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 codebase from one style to the other, but doing it little by little is perfectly valid and possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment