Skip to main content

The GDS Way and its content is intended for internal use by the GDS community.

Java style guide

The purpose of this style guide is to provide some conventions for working on Java code within GDS.

The Google Java style guide is a good starting point. The old Sun/Oracle Java style guide is still largely relevant but it has not been updated since 1999 and does not reflect recent changes to the language.

The more far-reaching Java for Small Teams ebook can be read online at no cost. While not free, Effective Java by Joshua Bloch (Addison-Wesley, 2017) is also an excellent resource. The GDS Library has physical copies of the book or you can buy a copy using the Learning and Development budget.

While the above resources are good guides, they may conflict with either each other or your team’s established practices. We favour consistency across our code, so make sure that you have the agreement of your team when considering using a new or different method or paradigm to those currently in use.

We generally use IntelliJ IDEA within GDS. Using it consistently helps when ensemble programming (pairing and mobbing). GDS have purchased licences for the commercial editions of IntelliJ IDEA for some teams. Some features of IntelliJ IDEA like Code With Me and JetBrains AI have not passed an information assurance review and must not be used (Full Line code completion can be used because it runs entirely on your device).

Code formatting

Variable and field names should match the names of their types (for example, an InputStream could be named inputStream or is), or have descriptive names reflecting the context of their use (for example, a Set<String> where each String is a username could be named usernames).

Always use curly braces around the body of an if, else, for, do or while statement, even if it’s only a single line. Follow the Kernighan and Ritchie (K&R) ‘Egyptian brackets’ style where there is no line break before the opening brace. See the braces section of the Google Java style guide for more details.

Use the GDS Way EditorConfig file, which has settings for things like code indentation. Place a copy of this file named .editorconfig in the root of your project to have IntelliJ IDEA and some other editors automatically apply the settings. If your editor does not support EditorConfig, manually configure its settings to match.

Some Java teams within GDS have had success using the Spotless auto-formatter. Some of the formatting styles supported by Spotless are quite opinionated and may want to make lots of changes if added to an existing project. The ratchetFrom option makes Spotless only format changed files (though this can require it to check out a lot of code in some cases, which may have performance implications if used as part of a build pipeline). Alternatively, you may wish to configure Spotless to match your existing conventions. For a new project, it’s probably easiest to use a style’s default settings.

Java EE and Jakarta EE

Java EE (Enterprise Edition), a collection of APIs used by many server-side Java applications, was spun out from Oracle and handed over to the Eclipse Foundation, who renamed it Jakarta EE due to not having the rights to the Java trademark (Jakarta is the largest city on the island of Java).

Beginning with version 9, Jakarta EE switched from using the javax package namespace to the jakarta package namespace, instantly breaking all applications that referenced the old package names.

Migrating from Java EE to Jakarta EE is hard. Many libraries and frameworks use Java EE or Jakarta EE so migrating completely may involve updating many of your dependencies to new versions that work with Jakarta EE (if they exist).

Some libraries and frameworks make this a little easier by having two parallel versions, one that works with Java EE and one that works with Jakarta EE, while being otherwise equivalent. This makes it possible to upgrade to the latest Java EE version of a dependency and then migrate across to its Jakarta EE equivalent in two separate steps.

If you are starting a new project, use Jakarta EE only from the outset unless you have a compelling reason not to.

Dependency injection (DI)

Consider whether a dependency injection framework is appropriate for your project before using it.

Some programmes use the Guice dependency injection framework. There are two current versions of Guice: Guice 6.0.0 supports Java EE and the javax package namespace while Guice 7.0.0 supports only Jakarta EE and the jakarta package namespace.

Imports

Wildcard imports should be avoided. They can cause existing code to break if a new type is added to a package with the same name as a type in another package.

This infamously happened with Java SE 1.2, which introduced java.util.List when there was already java.awt.List, breaking any classes that used List and imported both java.util.* and java.awt.*.

You should configure IntelliJ to explicitly import all classes and static methods in Settings → Editor → Code Style → Java → Imports with “Class count to use import with *” and “Names count to use static import with *” both set to a very high number, for example 1000.

Static imports should be avoided in most cases because the names of methods and constants often do not make sense without the context of the type they’re from.

Static imports are appropriate in some cases. Tests often read more fluently when assertion and matcher methods, which are well known and widely understood, are statically imported. For example, compare:

Assert.assertThat(actual, Is.is(expected));

… to:

assertThat(actual, is(expected));

Similarly, Math.max(…), Math.PI and ChronoUnit.DAYS could be statically imported because their names make sense on their own. However, LocalDate.of(…) should not be statically imported because the type it comes from is crucial for comprehension.

The IntelliJ ‘Optimize Imports’ (Ctrl+Option+O) command (in the Code menu) sorts imports and removes any that are unused. Before committing, you can select all the changed classes (for example, in the changes view of the commit tool window or the modified files pane of the commit dialogue) and then use this command to fix the imports for just the classes you modified.

Optionals

If a getter may return null, you should usually return an Optional instead.

Optional is intended to only to represent the absence of a result: do not use it for fields or method parameters. Java language architect Brian Goetz posted a StackOverflow answer explaining the intent of Optional with further reasoning.

You almost never need to use the isPresent() or isEmpty() methods on an Optional. Use ifPresent(…), ifPresentOrElse(…), map(…) or flatMap(…) instead. See DZone Java Zone’s article Optional isPresent() Is Bad for You for more details.

Optionals work best when used in a functional style, which can take time to learn. The DZone Java Zone article 26 Reasons Why Using Optional Correctly Is Not Optional has some guidance.

Local variable type inference (the var keyword)

You can use var in Java 10 onwards with local variables to have the compiler infer the type. This is especially good when invoking a constructor:

// Java 9
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

// Java 10+
var outputStream = new ByteArrayOutputStream();

It can also remove duplication where the return type and the variable name are the same:

// Java 9
CheckResponse checkResponse = service.getCheckResponse();

// Java 10+
var checkResponse = service.getCheckResponse();

Be mindful that var hides the type of the variable. If the variable name makes the type obvious, this is usually not a problem. But if it is not clear from either the variable name or the right-hand side of the assignment, it might be better to explicitly write the type.

If you are using the diamond operator in an assignment, you will usually find that updating it to use var also requires you to replace the diamond operator with the appropriate generic type parameter:

// usernames is Set<String>
Set<String> usernames = new HashSet<>();

// usernames is HashSet<Object> (probably not what you want)
var usernames = new HashSet<>();

// usernames is HashSet<String>
var usernames = new HashSet<String>();

The OpenJDK project has some style guidelines for local variable type inference. Oracle’s introduction to local variable type inference also contains some recommendations.

Records

Java 16 introduced record classes, which provide an excellent way to model immutable data with a concise syntax that eliminates the need to write tedious and error-prone code for basic functionality. Dev.java has a good introduction to records.

Prefer functionality in the Java standard library

Where possible, use functionality from the Java standard library rather than external libraries.

Keep in mind that improvements to the Java standard library mean that some external libraries which were popular in the past now add less value. For example, while Joda-Time was significantly better than the date and time libraries built into older Java versions, the java.time package introduced in Java 8 renders it redundant. Similarly, Google’s Guava is very useful (and recommended) but the unmodifiable collections built in to Java 9 largely remove the need for Guava’s immutable collections. The HttpClient introduced in Java 11 is good enough that you might not need a third party one.

When using external libraries, favour those which complement the Java standard library. For example, consider a library that introduces an object that behaves like a list. If this type does not implement java.util.List, it will not automatically benefit from the standard library’s support for features like streams.

Make sure any external library you use is appropriate for your purposes and avoid relying on internal implementation details of external libraries. If your IDE’s code completion suggests a method from an external library, make sure it’s a supported part of the library’s defined API.

Comments

Agree with your team to what extent you permit comments in your code. Some teams look more favourably on method-level and class-level Javadoc describing the context and responsibility of code, rather than inline comments.

It’s often possible to make code more readable so it does not need comments. The book Clean Code by Robert C Martin (Pearson, 2008) has some advice on how to do this.

Comments rarely need to describe what some code is doing or how it’s doing it. Comments explaining why the code is doing something, particularly if it’s non-obvious or requires external contextual knowledge, may be helpful.

When deciding whether or not something is non-obvious enough to require a comment, bear in mind that you are probably more familiar with the code you’re writing than most people who read it will be. Questions and misunderstandings from code reviewers can act as a guide (but keep in mind that other remedies such as renaming variables or methods may be better than adding comments).

Outdated comments can be worse than no comments at all because they may be misleading. Consider this before adding a comment and always keep comments up to date when changing code.

As an alternative to comments, commit messages can be a good place for describing why a particular piece of code was added, removed or modified. Tools like git-blame are built into most IDEs, allowing a developer to easily pull up the commit messages for revisions to a particular line of code. The age of each commit provides an indication of whether the commit message may be outdated.

Testing

Use JUnit for unit tests. It can also be used for many integration tests.

The latest JUnit 5 is not a drop-in replacement for JUnit 4. The JUnit Vintage test engine makes it possible to run JUnit 4 tests within JUnit 5, though not all JUnit 4 features are supported. If you use lots of JUnit 4 rules and alternative runners, or rely on other testing libraries that integrate with JUnit 4, you will have to make more changes. Teams within GDS have found it typically takes a few days to upgrade a project from JUnit 4 to JUnit 5 (keeping the test classes as JUnit 4 where possible). JUnit 4 continues to be maintained.

When you upgrade from JUnit 4 to JUnit 5, have a strategy for gradually making more and more of your test classes use JUnit 5. A good rule is to write all new test classes in JUnit 5 and to migrate over existing ones when you need to make major changes to them (generally it’s best to do the migration in a separate pull request first before making other changes).

For new projects, use JUnit 5 unless you have a reason not to.

Use Mockito for mocking. If you’re still using an older version, upgrading should be fairly straightforward: Mockito 3 contains no breaking changes, Mockito 4 only removes deprecated APIs and Mockito 5 mainly changes internals.

Code checking

We encourage the use of static analysis. Static analysis tools for Java include SonarQube, Codacy, CheckStyle and CodeQL. However, be aware that such tools can detect an overwhelming number of problems if applied to an existing project, which tends to result in their checks being ignored.

If possible, configure your static analysis tools using configuration files and keep these in your project repositories. This makes your settings more portable and makes it easier to perform the same checks in different places (for example, in a cloud service and on your own computer).

Some cloud-based static analysis tools, including Codacy, can work directly with GitHub repositories. Only grant a tool access to your repositories if it has been approved by an information assurance review. The Digital Identity programme has approval to use SonarCloud on public repos.

Try to minimise compiler warnings. If you cannot remove a warning, use an appropriate annotation to suppress it, preferably with a comment explaining why. For example:

public void frobulateFoos() {
    LOGGER.info("Frobulating foos");

    // LibFoo returns a raw list but every element is always a Foo
    @SuppressWarnings("unchecked")
    List<Foo> foos = (List<Foo>) fooService.getFoos();

    foos.forEach(Foo::frobulate);
}

Dependencies

Teams should agree their own rules for how to approve new dependencies and where dependencies can retrieved from (for example, Maven Central). When deciding whether or not to add a new dependency, consider its trustworthiness, longevity, licence and whether it appears to be actively maintained.

Try to keep up to date with the latest versions of your external dependencies. Older dependencies often contain security vulnerabilities. If you wait to upgrade your dependencies, you may find you have to make large version jumps to lots of dependencies at once, which can be painful. Frequent, smaller updates are almost always preferable.

Dependabot (which is part of GitHub) can automatically open pull requests to upgrade your libraries and other dependencies. Be aware that not all dependency upgrades are backwards compatible. Major version upgrades are more likely to cause problems than minor upgrades. Dependabot provides compatibility scores and links to release notes, which can help you make an informed decision. Do not merge a dependency upgrade unless it passes your automated tests. If it’s a major upgrade, monitor for any problems after it’s deployed.

Dependabot makes assumptions about version numbers that not all dependencies follow. This can cause it to open pull requests that update to beta versions, release candidates or even alternative variants of the dependency. Always have a human sense-check each Dependabot pull request before merging.

If you are not ready to upgrade to a particular version of a dependency, tell Dependabot to ignore that version by using a dependabot.yml file. A comment in the file explaining why it has been ignored can be useful. Using a dependabot.yml file is preferable to using @dependabot ignore commands because it’s much more visible and it’s also easier to tell Dependabot to stop ignoring a version upgrade.

There have been cases of bad actors raising malicious pull requests on repositories that appear to come from Dependabot. Make sure a pull request really comes from Dependabot before merging.

Dependabot only deals with direct dependencies, not transitive dependencies. Even if Dependabot thinks your project’s dependencies are fully up to date, you may still have outdated transitive dependencies (which may contain security vulnerabilities).

When viewing a Maven POM file, IntelliJ IDEA can warn you if your transitive dependencies have reported security vulnerabilities.

Build tools

You should use either Gradle or Maven as the build tool. Use recent versions if you can. Maven now supports the Bill of Materials (BOM) concept, which can simplify dependency management (Gradle also supports Maven BOMs).

Web frameworks

The Dropwizard web framework is used widely within GDS.

Dropwizard 3.0.0 and Dropwizard 4.0.0 were released simutaneously in March 2023. They have equivalent functionality but Dropwizard 3 continues to use Java EE and the javax package namespace while Dropwizard 4 migrates to Jakarta EE and the jakarta package namespace (Jakarta EE is the successor to Java EE). It is easier to upgrade to Dropwizard 3.0.x than to upgrade to Dropwizard 4.0.x.

Dropwizard 2 is no longer supported as of 31 January 2024.

Dropwizard has built-in support for validating requests with Hibernate Validator. Use Dropwizard’s validation in preference to rolling your own except in cases where Dropwizard’s built-in functionality cannot meet your validation requirements.

JDK

Use long-term support (LTS) releases

New major versions of the JDK are released twice a year (in March and September). Every two years, Oracle release a long-term support (LTS) version, which is maintained and receives updates for several years. Most other Java vendors follow Oracle’s lead, though their exact support lifecycles vary. Non-LTS releases usually stop receiving updates as soon as the next major version comes out. Within the Java ecosystem, many libraries and tools only really support and test against LTS releases.

For this reason, it is highly recommended to use an LTS release of Java. If you choose to use non-LTS releases, be aware that you will have to perform major version upgrades every six months and risk being stranded if libraries and tools you rely on are not prompt in adding support for the latest Java release.

If you are starting a new Java project, do not use anything older than the latest LTS release unless you have a good reason (for example, compatibility issues).

If you are currently using an older LTS release, you should be planning to upgrade to a later LTS version. There is usually an overlap of several years when both the current and some previous LTS releases are supported. Use this time wisely and balance the risk of upgrading before your tools and dependencies fully support a new LTS release against the risk of leaving the upgrade to the last minute. Bear in mind that your dependencies may stop supporting an LTS release before your JDK vendor does.

In general, upgrading between recent Java versions is a lot easier than it was a few years ago, due to better encapsulation within the JDK itself (making it harder for libraries to depend on implementation details that change from version to version) and because the ecosystem has gotten used to the rapid release schedule.

JDKs from different vendors

Recent versions of the Oracle JDK can be used free of charge for commercial and production purposes under the terms of a bespoke licence. OpenJDK is open source under the GPLv2 with the Classpath Exception but Oracle only provide general-availability OpenJDK builds for the latest release.

The Adoptium (formerly AdoptOpenJDK) project (part of the Eclipse Foundation) provides fully open-source TCK-certified pre-built OpenJDK binaries under the name Eclipse Temurin. For LTS releases (such as Java 8, 11, 17 and 21), Adoptium have committed to releasing free updates for several years.

In addition, Temurin has benefits such as being available in package repositories, having friendly installers for desktop use, and offering ready-made Docker images containing OpenJDK. It’s easy to install Temurin on your computer for development purposes using Homebrew or similar.

Amazon Corretto is a free OpenJDK distribution. The AWS Lamdba runtimes for Java use Corretto. Newer Java versions have significantly faster cold start times, which can be beneficial for Lambda.

Publishing artifacts

We recommend publishing artifacts (for which the source is already public) to Maven Central.

You should not use Maven Central to publish artifacts for which the source is closed or contains other proprietary assets, as it is a public repository with anonymous access. If you need to publish private artifacts for internal use, consider running your own Sonatype Nexus Repository.

Claiming group IDs

You will need to claim one or more group IDs in order to publish an artifact to Maven Central. The central repository is run and operated by Sonatype, and you will need to register for an account on their Jira instance to request control of a group ID.

The credentials used to create this account will be used during the publishing process, and should be stored safely and in accordance with any programme-specific guidance.

You should follow Sonatype’s guidance on registering for and claiming a group ID such as uk.gov.example. It is likely that you will be asked to prove your identity as a government actor, to prevent malicious parties publishing artifacts and claiming that they have been issued by the UK government.

Signing artifacts

Sonatype requires that artifacts published to their repository have been signed with a published GPG key. Each programme that wants to publish artifacts should create their own GPG key, publish it to a public keyserver (keys.openpgp.org and keyserver.ubuntu.com are recommended), then store the private key safely and in accordance with any programme-specific guidance.

The key and its associated passphrase will be used during the publishing process. This will require making it available safely to the task runner, for example Concourse, that is performing the deployment.

We recommend additionally publishing the public keys elsewhere, for example as a public GitHub repository, so that a third-party user knows an artifact is signed by a key that we have declared we use for signing.

Sonatype publishes build-tool-specific guidance for publishing and releasing artifacts on Maven Central.

This page was last reviewed on 23 July 2024. It needs to be reviewed again on 23 January 2025 by the page owner #java .
This page was set to be reviewed before 23 January 2025 by the page owner #java. This might mean the content is out of date.