Good code is 90% boilerplate

Good code is 90% boilerplate.

The sad truth is the better we get at writing code, the less fun it becomes. We learn about the SOLID principles, immutability, abstraction, composition, and maintainable code. But when we apply these techniques in a curly bracket language like Java or C#, it doesn't feel right. Pretty much all our code is boilerplate: we constantly repeat patterns and sections of code with little alteration between each class and project. Coding becomes incredibly monotomous, and requires a lot of typing to produce little functionality.

I've found that when people complain about using Java (which is quite a trendy thing to do) it's usually because of this: they've developed a way of writing maintainable code, but the language knows nothing about it, and is not expressive enough to describe it succintly.

We'll demonstrate this by building a toy application in Java and compare that approach with a more efficient, more enjoyable way of expressing the same ideas. At the end of the article we'll compare the two approaches side by side to justify the number in the title (but not before we encounter a shocking twist).

We're going to create a program that queries a database, gets a specific user by ID, and prints the number of letters in that user's name to the console. Not a terribly useful App, but it'll be enough to get my point across.

We'll look at code samples throughout the article, but I'd like to stress that you don't need to carefully analyse every line, it's more about just eyeballing the big picture class definitions and spotting the patterns.

A Java approach

So let's get started, it's Java so We begin by writing a class

public class UsernameLetterCountPublisher {
}

All Boilerplate. There's nothing specific to our App yet, well a name maybe, but creating the class is a task we repeat over and over with little alteration so I'm going to claim it's boilerplate.

The UsernameLetterCountPublisher unit will have a dependency that finds the user in the database. We want to couple the UsernameLetterCountPublisher class to its dependency as loosly as possible. This is mainly so we can test it without an actual database, but also so we're less likely to have to change it in the future. We define an interface, and inject an instance of this interface into the UsernameLetterCountPublisher's constructor, which now looks like this

public class UsernameLetterCountPublisher {

    private final UserProvider userProvider;

    public UsernameLetterCountPublisher(final UserProvider userProvider) {
        this.userProvider = userProvider;
    }
}

We will always inject a unit's dependencies through its constructor, so it's pretty much still all boilerplate. We define the UserProvider interface

public interface UserProvider {
  User execute(final String id);
}

and the User data type

public class User {
  public final String username;

  public User(final String username) {
    this.username = username;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    User user = (User) o;
    return Objects.equals(username, user.username);
  }

  @Override
  public int hashCode() {
    return Objects.hash(username);
  }

  @Override
  public String toString() {
    return "User{" + " username=" + username + "}";
  }
}


You could argue there is some application specific logic in the UserProvider interface, but I'd argue looking up an entity by ID is not application specific. I'd argue the only non-boilerplate here (besides naming) is the fact that a User object has a username field. The definition of the User class, e.g. how it's constructed with public final fields for immutability, and how its toString, equals and hashCode functions are defined, is the same for all our data structures.

An alternative approach: UnitilyLang

We are going to define our own language called UnitilyLang which is designed for the clean coding patterns we are using. There is a one to one mapping between the Java code and the UnitilyLang implementations, but the latter is much more succient. This highlights the boilerplate in the Java approach. With UnitilyLang we can replace the last two code sections (596 characters) with (30 characters)

data User
  username: String

where data describes a class with only public final fields, which are used to evaluate equals, hashCode, and toString, and are initialised through the class' constructor. User is the name of the class, username is its only field of type String. We'll see why we don't need the interface later in the article.

Now we need to create an implementation of the UserProvider interface, it will read from Dynamo DB (an AWS no SQL database that you don't need to know anything about).

public class DynamoDbUserProvider {
  private final ConfigProvider configProvider;

  private final UserTableProvider userTableProvider;

  private final DynamoItemReader dynamoReader;

  private final ItemToUserConverter itemToUserConverter;

  public DynamoDbUserProvider(
      final ConfigProvider configProvider,
      final UserTableProvider userTableProvider,
      final DynamoItemReader dynamoReader,
      final ItemToUserConverter itemToUserConverter) {
    this.configProvider = configProvider;
    this.userTableProvider = userTableProvider;
    this.dynamoReader = dynamoReader;
    this.itemToUserConverter = itemToUserConverter;
  }

  public User execute(final String id) {
    final Config config = configProvider.execute();
    final DynamoTable dynamoTable = userTableProvider.execute(config);
    final Item item = dynamoReader.execute(dynamoTable, id);
    final User user = itemToUserConverter.execute(item);
    return user;
  }
}

We call the DynamoDbUserProvider unit a workflow unit. Worklfow units don't do anything other than compose the execution of their dependencies. They are needed to create abstractions. e.g. we can have a single unit called DynamoDbUserProvider, whose functionality is obvious without us having to inspect its inner workings.

The DynamoDbUserProvider unit has four dependencies. I'm not going to show the dynamoReader code to keep the article as free of logic as possible. I've also not shown the ConfigProvider dependency code. Its implementation would typically read from environment variables or a file on disk, and provide an application level Config which is another data class. In our example Config looks like this

public class Config {
  public final DynamoTable userTable;

  public final String userId;

  public Config(final DynamoTable userTable, final String userId) {
    this.userTable = userTable;
    this.userId = userId;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Config config = (Config) o;
    return Objects.equals(userTable, config.userTable) && Objects.equals(userId, config.userId);
  }

  @Override
  public int hashCode() {
    return Objects.hash(userTable, userId);
  }

  @Override
  public String toString() {
    return "Config{" + " userTable=" + userTable + " userId=" + userId + "}";
  }
}

It has a DynamoTable field needed to identify a specific table, which is another data class

public class DynamoTable {
  public final String region;
  public final String tableName;
  public final String idFieldName;

  public DynamoTable(final String region, final String tableName, final String idFieldName) {
    this.region = region;
    this.tableName = tableName;
    this.idFieldName = idFieldName;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    DynamoTable that = (DynamoTable) o;
    return Objects.equals(region, that.region)
        && Objects.equals(tableName, that.tableName)
        && Objects.equals(idFieldName, that.idFieldName);
  }

  @Override
  public int hashCode() {
    return Objects.hash(region, tableName, idFieldName);
  }

  @Override
  public String toString() {
    return "DynamoTable{"
        + "region='"
        + region
        + '\''
        + ", tableName='"
        + tableName
        + '\''
        + ", idFieldName='"
        + idFieldName
        + '\''
        + '}';
  }
}

The DynamoDbUserStore also depends on a unit that provides the specific piece of config we need:

public class UserTableProvider {
  public DynamoTable execute(final Config config) {

    return config.userTable;
  }
}

and an ItemToUserConverter to convert the AWS DynamoDb SDK result (Item) to our data class User.

public class ItemToUserConverter {
  public User execute(final Item item) {

    String username = item.getString("username");
    User user = new User(username);
    return user;
  }
}

Once again we are drowning in boilerplate. The ConfigProvider implementation is reused in every App we build, the actual Config will change but the logic to create one doesn't, so I consider that boilerplate. The same is true for the DynamoItemReader dependency. Finally all our service classes have their dependencies injected through the constructors and stored as private final fields in all our projects.

The only real application specific logic in the DynamoDbUserProvider is naming and the the order in which its dependencies are composed together to obtain a User from an id.

Using UnitilyLang we could replace the previous five code sections (3006 characters plus the hidden ConfigProvider and DynamoItemReader classes we get for free in UnitilyLang) with (248 characters)

data DynamoTable
   region: String
   tableName: String
   idFieldName: String

data Config
  table: DynamoTable
  userId: String

pure ItemToUserConverter: Item -> User
item -> new User(item.getString("name"))

workflow DynamoDbUserStore
= 4 . (3 (2 1))

We declare Config and DynamoTable as a data classes just like we did for the User object.

To define the ItemToUserConverter we need our target language (in this case Java). UnitilyLang is designed around abstraction, immutability and composition. It's language agnostic, that is until we have to write logic. To define pure units we need to write some code.

Workflow units

The workflow declaration defines a class with private final fields for each of its dependencies injected through the constructor. The bottom line defines what the DynamoDbUserProvider does. Don't worry if the rest of this paragraph is unclear, my writing skills just aren't up to the job, we'll show it in a much clearer way in a moment. Anyway, let's give it a go... Each number n, corresponds to the nth dependency. The statement 4 . (3 (2 1)) defines how we compose those dependencies. We apply the second dependency to the result of the first. We then Partially apply the 3rd dependency and compose the resulting function with the 4th dependency. Declaring functions like this is known as zero point style.

The reason UnitilyLang can express the same meaning in considerable less code is simply that the Java programing langugage and libraries are not the most natural way of representing the clean code patterns we are following. UnitilyLang maybe more succint but it still doesn't feel a particulaly natural way of representing what the DynamoDbUserStore unit does i.e. how it composes its dependencies. The free text description in the previous paragraph certainly doesnt clear things up. So what is more natural? They say a picture is worth a thousand words, so lets draw the DynamoDbUserProvider.

This is equivalent to 4 . (3 (2 1)) but much easier to understand, Im so confident in this that I'm not going to explain it.

The interesting thing about the UnitilyLang (and picture) workflow units is that they are generic. They don't fix the types of their dependencies. As long as the types at the end of each arrow are the same when we instantiate one, it will compile. This actually restricts what our functions can do, and makes them easier to reason about. They can only use the results of the execution of their dependencies. It also means we dont need to declare interfaces, any set of dependencies whose types interact correctly can be used.

There is a one to one mapping between the Java and the UnitilyLang, and also between the UnitilyLang and the picture. The picture is far better than you may be thinking right now. Humans are infinitely better at understanding and reasoning about pictures than code. It's also far easier to refactor, if you want to change how the data flows, just rub out an arrow and redraw it to point somewhere else!

We've now written enough code to complete the UsernameLetterCountPublisher we started right at the begining of this article. We need to create its execute method to query the database, count the letters, and print a message to the screen.

public class UsernameLetterCountPublisher {
  private final ConfigProvider configProvider;

  private final UserIdProvider userIdProvider;

  private final UserProvider userProvider;

  private final UserMessageCreator userMessageCreator;

  private final Publisher publisher;

  public UsernameLetterCountPublisher(
      final ConfigProvider configProvider,
      final UserIdProvider userIdProvider,
      final UserProvider userProvider,
      final UserMessageCreator userMessageCreator,
      final Publisher publisher) {
    this.configProvider = configProvider;
    this.userIdProvider = userIdProvider;
    this.userProvider = userProvider;
    this.userMessageCreator = userMessageCreator;
    this.publisher = publisher;
  }

  public void execute() {
    final Config config = configProvider.execute();
    final String id = userIdProvider.execute(config);
    final User user = userProvider.execute(id);
    final String message = userMessageCreator.execute(user);
    publisher.execute(message);
  }
}

We'll quickly go over its dependencies. We reuse the ConfigProvider and inject a UserIdProvider

public class UserIdProvider {
    String getUserId(ApplicationConfig applicationConfig) {
        return applicationConfig.userId;
    }
}

to get a specific piece of config exactly like we did for loading the userTableName. We also inject a UserMessageCreator class

public class UserMessageCreator {
  public String execute(final User user) {

    String message =
        String.format("%s has %d letters in his/her name", user.username, user.username.length());
    return message;
  }
}

This unit is pure and coupled to the naming of its parent, so I dont always feel the need to reference it through a more abstract public interface. There is also a Publisher dependency. That isn't pure (it causes a side effect when it writes to the screen), We'll want to use multiple implementations e.g a mock for testing, so we do define an interface to inject.

public interface Publisher {
  void execute(final String x);
}

and create an implementation for the real app

public class ConsolePrinter {
  public void execute(final String x) {

    System.out.println(x);
  }
}

We call it the most generic name we can that fits the current classes purpose i.e. Publisher still makes sense if we choose to publish to a message queue instead of the console. We could then inject a MessageQueuePublisher instead of the ConsolePrinter, all without making any changes to the UsernameLetterCountPublisher class. The sharp-eyed will have noticed that we didn't tag the ConsolePrinter class with an implements Publisher statement, nor did we do this for the DynamoDbUserProvider implementation of the UserProvider interface. We'll explain why not when we come to instantiating units at the end of the article.

Again the majority of the UsernameLetterCountPublisher is boilerplate. You can see the repeated patterns by comparing it with the DynamoDbUserProvider. We need to define how we compose its dependencies (exactly as we did for the DynamoDbUserProvider), and there's some application specific logic in the UserMessageCreator and the ConsolePrinter, but thats all we should have to define.

So the previous 5 code sections (1545 characters) can be replaced with 274 characters of UnitilyLang

pure UserMessageCreator: User -> String
user -> String.format("%s has %d letters in his/her name", user.username, user.username.length())

sideeffect ConsolePrinter: String -> ()
message -> System.out.println(message)

workflow UsernameLetterCountPublisher
= 5 (4 (3 (2 1)))

We've seen similar declarations to UserMessageCreator and UsernameLetterCountPublisher before. The side effect declaration is similar to the pure unit declaration but informs us it will cause a side effect.

We can visualise the UsernameLetterCountPublisher by drawing it.

Unit and integration testing

We've now written all the code we need to do the task. We now need to write some tests and a program to run it. And guess what this is going to produce? thats right, more boilerplate.

We'll start with the pure units, here's a test for the UserMessageCreator

public class UserMessageCreatorTest {
  /** Variables */
  private static final User user;

  private static final String message;

  /** Test fixture */
  private UserMessageCreator userMessageCreator;

  static {
    final String username = "aUsersName";
    user = new User(username);
    message = "aUsersName has 10 letters in his/her name";
  }

  @BeforeEach
  public void setupTestFixture() {

    userMessageCreator = new UserMessageCreator();
  }

  @Test
  public void test1() {

    assertEquals(
        message,
        userMessageCreator.execute(user),
        "should create a message containing the number of characters in the username");
  }
}

and compare it with a test for our other pure unit ItemToUserConverter.

public class ItemToUserConverter {
  /** Variables */
  private static final Item item;

  private static final User user;

  /** Test fixture */
  private UserTableProvider userTableProvider;

  static {
    final String username = "aUsersName";
    item = new Item().withString("username", username);
    user = new User(username);
  }

  @BeforeEach
  public void setupTestFixture() {

    userTableProvider = new UserTableProvider();
  }

  @Test
  public void test1() {

    assertEquals(
        user,
        userTableProvider.execute(item),
        "Should convert a Dynamo SDK item to a User entity object.");
  }
}

See the duplication of patterns and code? more boilerplate! We always create some final static test data, then before each test, we create a new instance of the unit we are testing. Finally the test runs the unit's execute method and asserts its result is what we expect. UnitilyLang is optimised for this pattern, we simply define the unit(s) we are creating, the test data, and the assert(s). The previous two code sections (1286 characters) can be replaced with (484 characters)

testdata
  username = "aUsersName";
  user = new User(username);
  message = "aUsersName has 10 letters in his/her name";
unit
  UserMessageCreator  
asserts
  "should create a message containing the number of characters in the username" user -> message


testdata
  username = "aUsersName";
  item = new Item().withString("username", username);
  user = new User(username);
unit
  ItemToUserConverter
asserts
  "Should convert a Dynamo SDK item to a User entity object." item -> user

What about testing a unit whose dependencies cause side effects? e.g the DynamoDbUserProvider This is similar to testing the pure unit except we need to mock any dependencies that cause side effects.

public class DynamoDbUserProviderTest {
  /** Variables */
  private static final Item item;

  private static final User user;

  private static final String userId;

  private static final DynamoTable table;

  private static final Config config;

  /** Mocked dependencies */
  private ConfigProvider configProvider;

  private DynamoItemReader dynamoReader;

  /** Test fixture */
  private DynamoDbUserProvider dynamoDbUserProvider;

  static {
    final String username = "aUsersName";
    item = new Item().withString("username", username);
    user = new User(username);
    userId = "aUserId";
    table = new DynamoTable("region", "table", "idField");
    config = new Config(table, null);
  }

  @BeforeEach
  public void setupTestFixture() {
    configProvider = mock(ConfigProvider.class);
    dynamoReader = mock(DynamoItemReader.class);

    dynamoDbUserProvider =
        new DynamoDbUserProvider(
            configProvider, new UserTableProvider(), dynamoReader, new ItemToUserConverter());
  }

  @Test
  public void test1() {
    when(configProvider.execute()).thenReturn(config);
    when(dynamoReader.execute(table, userId)).thenReturn(item);

    assertEquals(
        user,
        dynamoDbUserProvider.execute(userId),
        "Should return a user created from the Item returned from Dynamo DB.");
  }
}

Lets compare this with a test for UsernameLetterCountPublisher

public class UsernameLetterCountPublisherTest {
  /** Variables */
  private static final String userId;

  private static final User user;

  private static final Config config;

  private static final String message;

  /** Mocked dependencies */
  private ConfigProvider configProvider;

  private UserProvider userProvider;

  private Publisher publisher;

  /** Test fixture */
  private UsernameLetterCountPublisher usernameLetterCountPublisher;

  static {
    userId = "aUserId";
    user = new User("aUsersName");
    config = new Config(null, userId);
    message = "aUsersName has 10 letters in his/her name";
  }

  @BeforeEach
  public void setupTestFixture() {
    configProvider = mock(ConfigProvider.class);
    userProvider = mock(UserProvider.class);
    publisher = mock(Publisher.class);

    usernameLetterCountPublisher =
        new UsernameLetterCountPublisher(
            configProvider,
            new UserIdProvider(),
            userProvider,
            new UserMessageCreator(),
            publisher);
  }

  @Test
  public void test1() {
    when(configProvider.execute()).thenReturn(config);
    when(userProvider.execute(userId)).thenReturn(user);

    usernameLetterCountPublisher.execute();
    verify(publisher).execute(message);
  }
}

again same patterns, same duplication, same boilerplate. Only now we create the unit with mocks and set up those mocks in the test. We can replace 2605 characters with 756 characters of UnitilyLang

testdata
  username = "aUsersName";
  item = new Item().withString("username", username);
  user = new User(username);
  userId = "aUserId";
  table = new DynamoTable("region", "table", "idField");
  config = new Config(table, null);
unit
  DynamoDbUserProvider
    -> config
    SubConfigProvider Config usersTable
    userId -> item
    ItemToUserConverter
asserts 
  "Should return a user created from the Item returned from Dynamo DB." userId -> user

testdata
  userId = "aUserId";
  user = new User("aUsersName");
  config = new Config(null, userId);
  message = "aUsersName has 10 letters in his/her name";
unit
  UsernameLetterCountPublisher
    -> config
    SubConfigProvider Config userId
    userId -> user
    UserMessageCreator
    message ->
-- No asserts so any dependencies without outputs will be verified

As we've seen throughout this article the UnitilyLang is more succint but can be more opaque. We can have the best of both worlds by drawing pictures. Let's draw the DynamoDbUserProviderTest

and the UsernameLetterCountPublisherTest

The grey boxes with unit names in represent concrete dependencies, the dashed boxes represent mocks, the labels define what test data the mocks expect and return.

Running a program

We now have code, and tests that validate it works. The last thing we need is a program. In Java we create a main function that creates the root unit (and all its dendencies) and executes it.

public class Main {
  public static void main(String[] args) {
    new UsernameLetterCountPublisher(
            new ConfigProvider(new EnvironmentProvider()),
            new UserIdProvider(),
            new UserProvider() {
              private final DynamoDbUserProvider dynamoDbUserProvider =
                  new DynamoDbUserProvider(
                      new ConfigProvider(new EnvironmentProvider()),
                      new UserTableProvider(),
                      new DynamoItemReader(new DynamoClientProvider()),
                      new ItemToUserConverter());

              public User execute(final String id) {
                dynamoDbUserProvider.execute(id);
              }
            },
            new UserMessageCreator(),
            new Publisher() {
              private final ConsolePrinter consolePrinter = new ConsolePrinter();

              public void execute(final String x) {
                consolePrinter.execute(x);
              }
            })
        .execute();
  }
}

and once again this is exactly the same as every other app we write. We would probably use a DI framework to create the units but this approach is perfectly valid for small apps. The only real interesting point here is that we create anonomous implmentations of interfaces e.g. new UserProvider(), doing this means we can use any class to implement an interface without tagging it with an implements. e.g. it's more similar to go or typescript where objects implement interfaces by default. Sure, it means theres a bit of untested logic creeping in, but i trust my self (or more importantly the IDE) to do this.

In UnitilyLang we declare the main entry point like this:

main UsernameLetterCountPublisher
    DynamoDbUserProvider
        ConfigProvider Config
        SubConfigProvider Config userTable
        DynamoItemReader
        ItemToUserConverter
    UsernameLetterCountPublisher
        ConfigProvider Config
        SubConfigProvider Config userId
        DynamoDbUserProvider
        UserMessageCreator
        ConsolePrinter

Or we can draw it

Conclusion (and shocking twist)

Im going to stop here rather than going on to maven files, build scripts, etc, which (spoiler alert) just lead to more boilerplate. At the bottom of this article we show the complete UnitilyLang project. It's 2106 characters. The total Java codebase is 22084 characters. So we can build the same project in UnitilyLang with 90% less code.

UnitilyLang reduces the amount of code we need, and UnitilyLang pictures simplify our projects. Think how much time it would take to write the Java code in this article, and compare it with the time taken to draw a few boxes. Think about how much easier it is to understand what the app does by looking at the previous picture compared with trawling through the Java repo. Now heres the shocking twist... I didn't write any of the Java code in this article, I generated it from the UnitilyLang pictures, and heres the video of me doing it.

Unitily is an application that generates code projects from the pictorial definition. You draw the pictures, it generates the code. Pictures are more intutitive, they are easier to create and easier to refactor. I infered earlier that refactoring Java is equivalent to rubbing out and re-drawing arrows in the picture. This is exactly what Unitliy allows you to do. If you want to extract a dependency, just draw a box and hook up the arrows.

You can have a closer look at the generated project on github and compare it with the UnitilyLang below. There is a more in depth demo playlist in the context of AWS Lambdas here. Finally you can read more about the clean code patterns we use in our other blogs.

UnitilyLang complete project

-- Units

data User
  username: String

data Config
  table: DynamoTable
  userId: String

pure ItemToUserConverter: Item -> User
item -> new User(item.getString("name"))

workflow DynamoDbUserStore
= 4 . (3 (2 1))

pure UserMessageCreator: User -> String
user -> String.format("%s has %d letters in his/her name", user.username, user.username.length())

sideeffect ConsolePrinter: String -> ()
message -> = System.out.println(message)

workflow UsernameLetterCountPublisher
= 5 (4 (3 (2 1)))

-- Tests

testdata
  username = "aUsersName";
  user = new User(username);
  message = "aUsersName has 10 letters in his/her name";
unit
  UserMessageCreator  
asserts
  "should create a message containing the number of characters in the username" user -> message


testdata
  username = "aUsersName";
  item = new Item().withString("username", username);
  user = new User(username);
unit
  ItemToUserConverter
asserts
  "Should convert a Dynamo SDK item to a User entity object." item -> user

testdata
  username = "aUsersName";
  item = new Item().withString("username", username);
  user = new User(username);
  userId = "aUserId";
  table = new DynamoTable("region", "table", "idField");
  config = new Config(table, null);
unit
  DynamoDbUserProvider
    -> config
    SubConfigProvider Config usersTable
    userId -> item
    ItemToUserConverter
asserts 
  "Should return a user created from the Item returned from Dynamo DB." userId -> user

testdata
  userId = "aUserId";
  user = new User("aUsersName");
  config = new Config(null, userId);
  message = "aUsersName has 10 letters in his/her name";
unit
  UsernameLetterCountPublisher
    -> config
    SubConfigProvider Config userId
    userId -> user
    UserMessageCreator
    message ->

-- App

main UsernameLetterCountPublisher
    DynamoDbUserProvider
        ConfigProvider Config
        SubConfigProvider Config userTable
        DynamoItemReader
        ItemToUserConverter
    UsernameLetterCountPublisher
        ConfigProvider Config
        SubConfigProvider Config userId
        DynamoDbUserProvider
        UserMessageCreator
        ConsolePrinter