Simpler Data Transfer Objects with Java Records

Simpler Data Transfer Objects with Java Records
Imaginea este preluată automat împreună cu articolul de pe Kaizen Driven Development

by Horațiu Dan

In very general terms, data transfer objects (DTOs) are structures that allow packing data when pieces of information are 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 “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 is straight forward (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 taking into account.

This article aims to present how a client application could elegantly organize and implement its DTOs using Java records, especially in 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 of BNP Paribas Open takes place, from the courtesy of the 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 DTOs 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-steps process:

  1. call GET https://api.weather.gov/points/{latitude},{longitude}
  2. 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 - [email protected]"

{
    "@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 - [email protected]"

{
    "@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, 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 - [email protected]")
                .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 next 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 handful way of organizing data transfer objects. As the DTOs may sometimes be very rich with regard to the encapsulated fields, records could save programmers a lot of time, as the boilerplate code is the compiler’s treat in this case.

Resources

[1] – National Weather Service AP

[2] – Article source code – record-dtos

[3] – The picture is from this year’s official collection