With JDK 16 reaching General Availability on 16 March 2021, Java records formally became a part of the language. Through retrofitting an existing application, we will take a closer look at what this feature provides, why it is useful, and how it can be used.


The JDK Enhancement Proposal describes records as “classes that act as transparent carriers for immutable data”. As such, records are conceptually similar to Kotlin data classes, Scala case classes, and, to some extent, Clojure records.

So, what do they look like? Here is a simple record Address:

That’s it. A record automatically comes with a canonical constructor and public accessors, as well as implementations of toString(), equals(), and hashCode(). The Address record above can be used like this:

Instances are immutable, so there are no mutators. Also, records are implicitly final and cannot be subclassed. Except for static fields, it is not possible to add any fields beyond what is already in the declaration. We are however free to add both static and non-static methods as needed, which we will see in the examples below.

Note that records is a feature delivered with the release of Java 16 — which is not a Long-Term-Support (LTS) release. If you decide to put this to real use, be prepared to update when Java 17 LTS comes along sometime in September 2021.

Use cases

Following are a few examples of where records can be a better alternative over ordinary Java classes:

Immutable domain objects

Historically, creating immutable objects in Java was rather painful work, records takes care of almost all of that work for us. Records also allows the class to be better focused on the business problem at hand by reducing boilerplate code. This makes it a compelling feature for implementing things like DDD-style Value Objects and Domain Events.

Now, if you want to go all the way and make everything immutable, you could do that too. But for a more functional style of programming you are probably better off with a language that fully supports persistent data structures, such as Clojure.


Some things are just unreasonably complicated to do in a language like Java, producing and parsing JSON are two of those things. Jackson is used pretty much universally for this purpose in Java, and generally its done by operating on Data Transfer Objects (DTOs) — classes created for this sole purpose.

Separating the DTOs from the core domain classes in our application might be a reasonable design principle; we want the loose coupling that allows us to evolve the API and the domain implementation independently, but also to avoid compromising the domain model’s integrity just to make JSON serialization fit an external API. As a result, we end up with a lot of classes that don’t really do much but act as intermediaries between JSON and our domain model implementation. While records doesn’t free us from these classes per se, they can at least help out with the implementation and reduce the lines of code we need to keep track of.

Temporary containers of data

Records can be defined not only as stand-alone classes, but also locally inside a method. This makes them useful as temporary containers during data processing, for quickly creating ephemeral mock data in tests, etc. We will see an example of this below.

Photo by Thom Holmes on Unsplash

Records in practice

To further explore records, we will look at Guessing Game — an existing Dropwizard web application that has been retroftitted using records. Guessing Game is part of the sample applications developed by Serialized to showcase how their managed platform can be used for building Event Sourced/CQRS style applications.

As such, the application is developed using CQRS, leveraging the Serialized platform as event store and projection engine. In this style of design, state transitions are driven exclusively by events. The current state of the application is stored, not as a snapshot in a database, but as a series of events in an append-only event store. This series of events tells the complete story of how the application ended up in its current state.

The CQRS part implies that writing to the application is separate from reading from the application; in practice writing is done by issuing POST commands to the application’s HTTP API command endpoint, and reading is done by issuing GET requests to a separate HTTP API query endpoint. We don’t need to go into further detail here, but there are a number of resources available for learning more about Event Sourcing and CQRS, for example here, here, and here.

While interacting with the Serialized platform can be done entirely over HTTP, the example application makes heavy use of the Serialized Java client to rid us of some of the lower-level plumbing.

The updated source code is in a fork on GitHub, and to follow along we need JDK 16 or later, a free Serialized account, and optionally, an IDE with Java 16 support, such as IntelliJ IDEA 2021.1.

Guessing Game

The concept of the Guessing Game is simple enough: When the game starts, it generates a random number between 1 and 100, and the player’s objective is to guess the number in the least number of attempts, but no more than 10.

Instructions on how to get started and how to issue write commands and read queries are all in the readme.

Applying Records

The architectural emphasis on making events and commands explicit concepts comes with a number of benefits, but also with the work of implementing these concepts in code. Fortunately for us, records is a good match for this as these abstractions fit nicely into the first two use case categories outlined above: Immutable domain objects and DTOs.

Let’s start with a command. The commands in this application are also the DTOs used for JSON deserialization as the commands arrive over the HTTP API. The command used to guess the number is GuessNumberCommand:

And it’s records counterpart:

Now, since the original implementation was pretty bare-bones already, the records version doesn’t exactly showcase a dramatic difference. But it illustrates nicely how records integrate with annotations. Annotations on records are added to the elements in the class where they are applicable, mainly guided by the annotations’ @Target.

JSON serialization works out of the box with no additional annotations when using Jackson version 2.12 or later.

So, let’s look at an event. Events are emitted by the Game class as the game transitions into a new state, while the actual current state of the game is contained in the associated GameState class. Here is the GameFinished event, emitted as the game transitions into its final state:

An event is typically considered an essential part of the domain model, it represents “something that happened that the domain experts care about”. While domain events have an explicit identity, they are also immutable. An event represents something that has happened, as such, it will never change after the fact. We’ll sort this example into the Immutable Domain Objects category:

The records version is shorter with attribute definitions condensed and accessors automatically create for us. Granted, the equals() method does a bit of overreaching here as it probably had been sufficient to look only at the UUID, but since events are immutable we should be fine. It might come across as a subtle point, but when using records it is important to ensure that the default implementations provided actually do what we want — and if they don’t, maybe it’s not a record after all.

As shown in this example, it’s perfectly fine to add methods to provide additional functionality. Records can be more than simple data containers.

The GameState class handles events to reflect the games current state:

In the original implementation above, this is a mutable object. But looking further, there is no obvious reason why it has to be. It certainly has a Value Objects flavour to it. Here is what an immutable GameState class using records can look like:

The GameState class is used by the Serialized Java client for loading current state by applying historical events stored in the event store, as such it comes with a couple design constraint: The handle-methods need to return a reference to the state, and there has to be a no-arguments constructor.

The no-args constructor has been added to our record, setting up the default initial state. The handle-methods now return a new class instance instead of a reference to itself.

If we have a modelling choice, skewing towards immutability is arguably a good rule of thumb. If we are loading a significant number of events, this will put the garbage collector to work, but in many situations (as in the case with this game) this is likely not going to be an issue.

Finally, let’s look at a test:

This test checks the expected behaviour of the query API endpoint on our service, returning game history. As the actual game history is generated by the Serialized projection engine called by our service, we have stubbed the response from the Serialized platform. Using two local records allows us to quickly pull together the required data for properly stubbing the response.

That’s it! More examples can be found in the full source code on GitHub.


Records is an intriguing new feature added to the Java language with JDK 16. Use it to explicitly model immutable values in your Java programs while at the same time reduce boilerplate code. It is particularly useful for Immutable domain objects, DTOs, and local containers of short-lived data.

Developing software, products, teams, and organizations

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store