Simpler Data Transfer Objects With Java Records
See how applications could elegantly organize and implement DTOs using Java records, especially in case of read-only operations.
Join the DZone community and get the full member experience.
Join For FreeIn very general terms, data transfer objects (DTOs) are structures that allow packing data when information is exchanged among applications or processes. While business objects or even entities own both state and behavior, DTOs should have only state. I personally see them as the apparel that the domain, the application's “center of purity,” puts on when engaging in interactions with the “exterior.”
Java records, on the other hand, prove very useful when data-oriented structures are needed, as a lot of boilerplate code is removed.
In case of the widely used REST APIs, a service implementer exposes various details to its clients and allows them to perform operations of interest. In the simple use case of a weather forecast service, even if a lot of pieces of information are exposed, most probably, there are more in its backend. Nevertheless, as the responses here are very verbose, most probably, client applications usually do not use all these details. Consequently, DTOs will be structured to pack only what’s needed.
Moving forward, even if the design and implementation of the DTOs in such cases are straightforward (with POJOs or Lombock, for instance), I consider that with the introduction of the Java records, a newer, simpler, and better-integrated option is worth considering.
This article aims to present how a client application could elegantly organize and implement its DTOs using Java records, especially in the case of read-only operations.
Proof of Concept
Let’s imagine the place of interest here is Indian Wells, California, USA. Why this one? Because this is where the annual professional tennis tournament, the BNP Paribas Open, takes place, courtesy of Oracle Co-Founder and tennis enthusiast Larry Ellison. Since I am an avid tennis fan, but the Grand Slams always catch everybody’s attention, I decided to choose this one for this POC.
In order to enjoy the matches, both as a player and a spectator, a weather forecast would be useful up front. In this direction, weather.gov REST API is used to get the conditions for a few days in advance.
The focus here, though, is not on tennis but on the data received and, thus, on the client DTO's design.
Implementation
The proof of concept uses Java 21, Spring Boot version 3.4.4, specifically RestClient, and Maven version 3.9.9.
The endpoints of interest can be used free of charge, yet a User-Agent header is required to identify the client application.
Retrieving the forecast is a two-step process:
- Call GET
https://api.weather.gov/points/{latitude},{longitude}
- Call GET
https://api.weather.gov/gridpoints/{officeId}/{x},{y}/forecast
The former endpoint requires as input the coordinates, while the latter is a little bit more cryptical. Luckily, the response of the former, contains the prepopulated URL of the latter.
When calling the first, a quite verbose response is retrieved.
curl -X GET "https://api.weather.gov/points/33.7179,-116.3431" -H "Accept: application/geo+json" -H "User-Agent: MyApp - horatiucd@gmail.com"
{
"@context": [
"https://geojson.org/geojson-ld/geojson-context.jsonld",
{
"@version": "1.1",
"wx": "https://api.weather.gov/ontology#",
"s": "https://schema.org/",
"geo": "http://www.opengis.net/ont/geosparql#",
"unit": "http://codes.wmo.int/common/unit/",
"@vocab": "https://api.weather.gov/ontology#",
"geometry": {
"@id": "s:GeoCoordinates",
"@type": "geo:wktLiteral"
},
"city": "s:addressLocality",
"state": "s:addressRegion",
"distance": {
"@id": "s:Distance",
"@type": "s:QuantitativeValue"
},
"bearing": {
"@type": "s:QuantitativeValue"
},
"value": {
"@id": "s:value"
},
"unitCode": {
"@id": "s:unitCode",
"@type": "@id"
},
"forecastOffice": {
"@type": "@id"
},
"forecastGridData": {
"@type": "@id"
},
"publicZone": {
"@type": "@id"
},
"county": {
"@type": "@id"
}
}
],
"id": "https://api.weather.gov/points/33.7179,-116.3431",
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-116.3431,
33.7179
]
},
"properties": {
"@id": "https://api.weather.gov/points/33.7179,-116.3431",
"@type": "wx:Point",
"cwa": "SGX",
"forecastOffice": "https://api.weather.gov/offices/SGX",
"gridId": "SGX",
"gridX": 94,
"gridY": 53,
"forecast": "https://api.weather.gov/gridpoints/SGX/94,53/forecast",
"forecastHourly": "https://api.weather.gov/gridpoints/SGX/94,53/forecast/hourly",
"forecastGridData": "https://api.weather.gov/gridpoints/SGX/94,53",
"observationStations": "https://api.weather.gov/gridpoints/SGX/94,53/stations",
"relativeLocation": {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-116.341248,
33.692217
]
},
"properties": {
"city": "Indian Wells",
"state": "CA",
"distance": {
"unitCode": "wmoUnit:m",
"value": 2860.9572565499
},
"bearing": {
"unitCode": "wmoUnit:degree_(angle)",
"value": 356
}
}
},
"forecastZone": "https://api.weather.gov/zones/forecast/CAZ061",
"county": "https://api.weather.gov/zones/county/CAC065",
"fireWeatherZone": "https://api.weather.gov/zones/fire/CAZ261",
"timeZone": "America/Los_Angeles",
"radarStation": "KNKX"
}
}
For the second step, only the forecast URL is of interest and available in the properties object.
"forecast": "https://api.weather.gov/gridpoints/SGX/94,53/forecast"
When calling it, another verbose response is returned.
curl -X GET "https://api.weather.gov/gridpoints/SGX/94,53/forecast" -H "Accept: application/geo+json" -H "User-Agent: MyApp - horatiucd@gmail.com"
{
"@context": [
"https://geojson.org/geojson-ld/geojson-context.jsonld",
{
"@version": "1.1",
"wx": "https://api.weather.gov/ontology#",
"geo": "http://www.opengis.net/ont/geosparql#",
"unit": "http://codes.wmo.int/common/unit/",
"@vocab": "https://api.weather.gov/ontology#"
}
],
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-116.3188,
33.7128
],
...
]
]
},
"properties": {
"units": "us",
"forecastGenerator": "BaselineForecastGenerator",
"generatedAt": "2025-04-11T12:02:01+00:00",
"updateTime": "2025-04-11T08:11:47+00:00",
"validTimes": "2025-04-11T02:00:00+00:00/P7DT23H",
"elevation": {
"unitCode": "wmoUnit:m",
"value": 45.1104
},
"periods": [
{
"number": 1,
"name": "Overnight",
"startTime": "2025-04-11T05:00:00-07:00",
"endTime": "2025-04-11T06:00:00-07:00",
"isDaytime": false,
"temperature": 71,
"temperatureUnit": "F",
"temperatureTrend": "",
"probabilityOfPrecipitation": {
"unitCode": "wmoUnit:percent",
"value": null
},
"windSpeed": "0 mph",
"windDirection": "",
"icon": "https://api.weather.gov/icons/land/night/skc?size=medium",
"shortForecast": "Clear",
"detailedForecast": "Clear, with a low around 71. West wind around 0 mph."
},
....
]
}
}
For the purpose of this POC, the coming forecast ‘periods’ are the main goal here. After a quick service exploration, the DTOs may be sketched and the client application built.
In order to model the first response, only the necessary pieces of information are highlighted,
{
"properties": {
"forecast": "https://api.weather.gov/gridpoints/SGX/94,53/forecast"
}
}
Then translated into the following record:
public record PointMetadata(Properties properties) {
public record Properties(String forecast) {}
}
For the second, the pieces of information of interest are:
{
"properties": {
"periods": [
{
"number": 1,
"name": "Overnight",
"startTime": "2025-04-11T05:00:00-07:00",
"endTime": "2025-04-11T06:00:00-07:00",
"temperature": 71,
"temperatureUnit": "F",
"windSpeed": "0 mph",
"windDirection": "",
"shortForecast": "Clear",
"detailedForecast": "Clear, with a low around 71. West wind around 0 mph."
},
....
]
}
}
... and modeled as below:
public record Forecast(Props properties) {
public record Props(List<Period> periods) {}
public record Period(String name,
String startTime, String endTime,
int temperature, @JsonProperty("temperatureUnit") String unit,
String windSpeed, String windDirection,
String shortForecast,
String detailedForecast) {}
}
That’s all the code. The focus is on the actual fields, while the compiler takes care of the boilerplate code for us.
Didactically, the retrieved temperatureUnit
was renamed to unit, to outline this possibility, in case a client application does not like the names of the exposed properties. By all means, this makes a lot of sense, since at some point I heard someone saying that one of the hardest programming tasks is naming variables.
The DTOs exist, and the client service can be implemented as well.
@Service
public class ForecastService {
private final RestClient restClient;
public ForecastService() {
restClient = RestClient.builder()
.baseUrl("https://api.weather.gov")
.defaultHeader("User-Agent", "MyApp - horatiucd@gmail.com")
.defaultHeader("Accept", "application/geo+json")
.build();
}
public Forecast forecast(double latitude, double longitude) {
PointMetadata point = restClient.get()
.uri("/points/{latitude},{longitude}", latitude, longitude)
.retrieve()
.body(PointMetadata.class);
if (point == null || point.properties() == null) {
throw new RuntimeException("Unable to retrieve forecast point meta-data");
}
return restClient.get()
.uri(point.properties().forecast())
.retrieve()
.body(Forecast.class);
}
}
When run, the next unit test allows displaying the forecast for a set of coordinates, here, the ones of Indian Wells – (33.7179, -116.3431).
class ForecastServiceTest {
private ForecastService forecastService;
@BeforeEach
void setUp() {
forecastService = new ForecastService();
}
@Test
void forecast() {
final double latitude = 33.7179;
final double longitude = -116.3431;
Forecast forecast = forecastService.forecast(latitude, longitude);
Assertions.assertNotNull(forecast);
forecast.properties().periods().forEach(period ->
System.out.println(period.name() + ": " + period.startTime() + " - " + period.endTime());
+ ": " + period.detailedForecast()));
}
}
At the time of this writing, the results retrieved show the weather in the following days seems pretty good for tennis, both for playing and watching.
Today, 2025-04-11T08:00:00-07:00 - 2025-04-11T18:00:00-07:00: Sunny, with a high near 101. Southeast wind around 5 mph.
Tonight, 2025-04-11T18:00:00-07:00 - 2025-04-12T06:00:00-07:00: Partly cloudy, with a low around 67. Northwest wind 0 to 10 mph.
Saturday, 2025-04-12T06:00:00-07:00 - 2025-04-12T18:00:00-07:00: Mostly sunny, with a high near 97. Southwest wind 0 to 10 mph.
Saturday Night, 2025-04-12T18:00:00-07:00 - 2025-04-13T06:00:00-07:00: Partly cloudy, with a low around 64. Northwest wind 0 to 15 mph, with gusts as high as 25 mph.
Sunday, 2025-04-13T06:00:00-07:00 - 2025-04-13T18:00:00-07:00: Sunny, with a high near 95. South wind 0 to 5 mph.
Sunday Night, 2025-04-13T18:00:00-07:00 - 2025-04-14T06:00:00-07:00: Partly cloudy, with a low around 66.
Monday, 2025-04-14T06:00:00-07:00 - 2025-04-14T18:00:00-07:00: Mostly sunny, with a high near 93.
Monday Night, 2025-04-14T18:00:00-07:00 - 2025-04-15T06:00:00-07:00: Partly cloudy, with a low around 63.
Tuesday, 2025-04-15T06:00:00-07:00 - 2025-04-15T18:00:00-07:00: Sunny, with a high near 91.
Tuesday Night, 2025-04-15T18:00:00-07:00 - 2025-04-16T06:00:00-07:00: Mostly clear, with a low around 63.
Wednesday, 2025-04-16T06:00:00-07:00 - 2025-04-16T18:00:00-07:00: Sunny, with a high near 91.
Wednesday Night, 2025-04-16T18:00:00-07:00 - 2025-04-17T06:00:00-07:00: Mostly clear, with a low around 62.
Thursday, 2025-04-17T06:00:00-07:00 - 2025-04-17T18:00:00-07:00: Sunny, with a high near 89.
Thursday Night, 2025-04-17T18:00:00-07:00 - 2025-04-18T06:00:00-07:00: Mostly clear, with a low around 61.
Conclusion
With their succinct appearance and data-oriented characteristics, Java records prove to be a very useful way of organizing data transfer objects. As the DTOs may sometimes be very rich regarding the encapsulated fields, records could save programmers a lot of time, as the boilerplate code is the compiler’s treat in this case.
Resources
- National Weather Service AP
- Article Source Code
- The picture is from this year’s official collection
Published at DZone with permission of Horatiu Dan. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments