The Simplicity of Opinionated Tooling

I’ve used a variety of tooling - IDEs, build systems, issue tracking, source control, etc. - in my career thus far. It seems like the general direction with many things in software has been that they grow more opinionated to simplify the “happy path” use case. I’m all for this.

The State of Things

I inherited a fairly aged codebase when I started at my current job with the State of Alaska. For example, one of the 3 applications my team owns has git history going back to 2007. As you might expect, the codebase seems to have a lot of inertia in regards to its tooling. It uses ant for building, ivy for dependencies, eclipse for the IDE, etc. The other two applications are a similar story, but a slightly smaller scale.

The first thing I decided to tackle when I got started was trying to modernize the build system for the medium-size one of the 3.

Each of the 9 sub-projects had an ant build.xml file - the largest being almost 1,000 lines long. No big deal, someone had even pre-pended numbers to the project names so you knew what order to run them in! Even better, developers didn’t need to touch these that often. Or at least, they wouldn’t if the code wasn’t being signed with a SHA1 cert, or the application was running properly on their machine.

This seemed like the closest large fire, so I got to work figuring it out.

The cert thing ended up being trivial, just had to find the needle in the ant haystack, change a few parameters (and the JRE so java knew what to do with the shiny modern signing algorithm) and we were able to use modern Java 1.8 (just ignore that oxymoron).

Getting it running on a developer machine? The hack I first did was pretty easy too - just changed the port in an app.properties to listen on the TomEE port instead of the WildFly port (for some reason the dev environment had you running both) and it worked!

I now had to live with the 2300-line monster of an ant build, or do something about it. Enter build tooling!

What options do you really have?

There are a quite a few solutions to getting a JAR, a WAR, or an executable out of any given set of java code. At the most basic, you can just write a really long bash script. This isn’t really recommended with the size of projects most people deal with. Ant is a pretty logical next step up in abstraction, but still has a lot of the drawbacks of a bash script or a makefile calling javac: its verbose, doesn’t enable incremental builds, and can act entirely different on different workstations.

In Java land, you really end up finding two recommendations for your build tooling: Apache Maven and Gradle.

Lets start with Maven. Like Ant before it, Maven is defined in xml files. The first immediate divergence is the presence of dependency definitions. Maven handles builds through defining various phases that you can target via its CLI. Each phase contains a set of goals that function as specific tasks that need to be executed. Most of the time these take the form of plugins. For multi-module projects, you can define each one as a maven module, then have an aggregate parent task. For the most part, Maven has built-in defaults for the common Java project types, which reduces some of the boilerplate that you would have to write for Ant. It also cleanly solves the dependency management aspect that Ant doesn’t even try and touch. This is great, but still doesn’t address all of our issues (at least out of the box).

This leads us to Gradle, the youngest and most full-featured of the tools we will talk about. Building upon the work that had been done with Ant and Maven, Gradle brings in two main concepts that really set it apart to me: incremental builds and good handling of tooling.

Incremental builds aren’t directly related to gradle being “opinionated” - but they are a standout feature. Each task in gradle has its inputs and results tracked. Next time you run a task that depends on one of those, it won’t re-run the unchanged tasks (unless their inputs have changed). In a 14 sub-project application, this can make a big difference if you are only working in one project most of the time.

Gradle also has some built-in tooling to handle downloading itself and the toolchains to build your project. Need to set up a new developer workstation? Just run the gradle wrapper, no pre-installation needed. Need to switch java versions? Just tell gradle to download Java for you, and then change the version number for your project.

Convention over Configuration

Both Maven and Gradle follow this idea that convention should be preferred over configuration. In more words, there should be a preferred way of doing things, and the tool will be easier if you follow it. But if you have to do things differently, you can configure it to allow for this.

Build tooling was not the first place I heard of convention over configuration. It’s been around in the web framework space for a while - check out the Rails Doctrine if you don’t believe me. The basic idea is that most of your project structure isn’t going to be that much different from everyone else’s, so that structure should be easy to implement. In Web land, this looks kinda like:

I have a /users endpoint in an MVC App. The controller probably should be named UsersController, and have the default page be /views/users/index.html. So my web framework will look there for those two things. Lets say you’d rather call it the PeopleController - great! Just add an attribute on your controller to specify that it handles the /users endpoint.

Ok, well that is the web, so what does that mean for our build tooling?

I’m going to focus on Gradle here, but the the directory structure comes from Maven.

Let’s say you have an application in 3 sub-projects: backend, frontend, and shared

You’ll have some basic setup stuff in gradle files at the top level. Then each project folder should have the same structure

project
    src
        main
            java
                com/example/stuff
                    Things.java
                    ...
        test
            java
                com/example/stuff/test
                    TestThings.java
                    ...

Assuming you just want a jar of all that stuff, you can have a pretty simple build.gradle file

plugins {
    id `java`
}

Run ./gradlew :project:jar and you have a jar with all your classes!

Need some resources? Well, you can stick them in src/main/resources/com/example/stuff and gradle will jar them up for you, because that is where they should go.

So yeah, this might save you a lot of boilerplate, but that can’t be the whole point of it, right?

I like how DHH puts it in the Rails doctrine:

Part of the Rails’ mission is to swing its machete at the thick, and ever growing, jungle of recurring decisions that face developers

We as developers need to be spending our brain cycles solving the problems that actually matter. So lets come out of the build tooling world a bit and talk about formatters. The whole “tabs vs. spaces” debate is a fun thing to joke about, but for an actual working team, at some point you have to decide. Or do you? Rather than decide each individual item like that, you can just agree on an opinionated formatter, then set up format on save in your IDE.

Does one person on the team constantly use yoda notation without caring that it’s against the style guide? Use a static analysis tool and make that into a build error.

Almost every step of the development cycle can have some sort of tool that makes it easier for you to do the “right” thing and spend your time focusing on writing useful code.

So how’d that ant build end up?

I’m pretty happy with the gradle solution I came up with. It’s not petite - a few hundred lines of Groovy - but its a lot more expressive and concise than the ant build was. Ant is still in there - signing jars is something that Gradle doesn’t seem to want to do natively - but even the ant parts are a lot more readable.

Software Development in the Rainforest

Programming, DIY, Hiking and more


Why I let my tools make decisions for me

By Luke Ebersole, 2023-10-30