Skip to content

Instantly share code, notes, and snippets.

@brettanomyces
Last active April 20, 2022 09:56
Show Gist options
  • Save brettanomyces/5cde1d4f4b61a41fe797cc9bb2fd833c to your computer and use it in GitHub Desktop.
Save brettanomyces/5cde1d4f4b61a41fe797cc9bb2fd833c to your computer and use it in GitHub Desktop.

Pervasive Package Problems

An approach that is often used for organising our classes into packages is to create a package per stereotype.

With a package for all of our service classes, a package for all our repository classes, a package for our clients, a package for all our configuration classes, etc.

I think this approach makes intuitive sense, which is why we see it so often. However, I think it leads to undesirable outcomes for our codebase.

Say we have an application with two components; Foo and Bar. Here’s what a package per stereotype structure looks like:

.
`-- src/main/java/com/foobar
    |-- client
    |   |-- FooClient.java
    |   |-- BarClient.java
    |-- config
    |   |-- FooConfig.java
    |   |-- BarConfig.java
    |-- service
    |   |-- BarService.java
    |   |-- BarInterface.java
    |   |-- FooInterface.java
    |   |-- FooService.java
    `-- util
        |-- FooUtil.java
        `-- BarConverter.java

All interaction with Foo and Bar happens via the interfaces FooInterface and BarInterface. These interfaces are implemented by FooService and BarService. FooService has two dependencies; FooClient and FooUtil. Bar service also has two dependencies; BarClient and BarConverter.

We are using Spring so in our config files we register a @Bean for FooInterface and BarInterface.

class FooConfig {
  
  @Bean
  FooInterface foo() {
    return new FooService(
      new FooClient(),
      new FooUtil()
    );
  }
}

Can you see the problem? Unfortunately it’s hard to spot, and only once your application has grown large will you start feeling the effect.

Because the configuration files lives in their own package, all the classes they make use of must be made public so they can be accessed by the configuration.

Ideally only FooInterface would be public. All interaction with Foo would be done via the Foo interface. FooService, FooClient, FooUtil would be hidden from the rest of the application, treated as internal implementation details.

When classes are public they may be used anywhere in the codebase. BarService may start using FooUtil for example. This is bad because it couples Foo and Bar. We can no longer refactor the internals of Foo without possibly breaking Bar. Left unchecked this will lead to our code becoming the proverbial big ball of mud.

How should we structure our packages?

Package per component.

.
`-- src/main/java/com/foobar
    |-- foo
    |   |-- FooClient.java  // package private
    |   |-- FooConfig.java  // package private
    |   |-- FooInterface.java  // public
    |   |-- FooService.java  // package private
    |   |-- FooUtil.java  // package private
    `-- bar
        |-- BarClient.java  // package private
        |-- BarConfig.java  // package private
        |-- BarInterface.java  // public
        |-- BarService.java  // package private
        `-- BarConverter.java  // package private

Some of our codebase are structured like this but fail to make classes package private where they could. This means BarService can still make use of FooUtil.

I think this partly is a problem with how Java is taught; we default to making all our classes public. Take a look at any tutorial on https://www.baeldung.com/. It may also be partly due to the lack of an explicit package private keyword.

Don’t despair. We can overcome our poor intuition and bad habits.

Package per component instead of package per stereotype.

Default to package private classes instead of public

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