DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Distribution Design Patterns in Java - Data Transfer Object (DTO) And Remote Facade Design Patterns
  • Writing DTOs With Java8, Lombok, and Java14+
  • Redefining Java Object Equality
  • Dynamic File Upload Component in Salesforce LWC

Trending

  • Traditional Testing and RAGAS: A Hybrid Strategy for Evaluating AI Chatbots
  • Code Reviews: Building an AI-Powered GitHub Integration
  • Supervised Fine-Tuning (SFT) on VLMs: From Pre-trained Checkpoints To Tuned Models
  • How to Create a Successful API Ecosystem
  1. DZone
  2. Coding
  3. Java
  4. Simpler Data Transfer Objects With Java Records

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.

By 
Horatiu Dan user avatar
Horatiu Dan
·
May. 28, 25 · Analysis
Likes (1)
Comment
Save
Tweet
Share
2.6K Views

Join the DZone community and get the full member experience.

Join For Free

In 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:

  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.

PowerShell
 
curl -X GET "https://api.weather.gov/points/33.7179,-116.3431" -H "Accept: application/geo+json" -H "User-Agent: MyApp - horatiucd@gmail.com"
JSON
 
{
    "@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. 

JSON
 
"forecast": "https://api.weather.gov/gridpoints/SGX/94,53/forecast"


When calling it, another verbose response is returned.

PowerShell
 
curl -X GET "https://api.weather.gov/gridpoints/SGX/94,53/forecast" -H "Accept: application/geo+json" -H "User-Agent: MyApp - horatiucd@gmail.com"
JSON
 
{
    "@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,

JSON
 
{    
    "properties": {        
        "forecast": "https://api.weather.gov/gridpoints/SGX/94,53/forecast"
    }
}


Then translated into the following record: 

Java
 
public record PointMetadata(Properties properties) {
 
    public record Properties(String forecast) {}
}


For the second, the pieces of information of interest are:

JSON
 
{    
    "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: 

Java
 
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.

Java
 
@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). 

Java
 
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. 

Plain Text
 
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

  1. National Weather Service AP
  2. Article Source Code
  3. The picture is from this year’s official collection
Java (programming language) Object (computer science) Record (computer science) Transfer (computing)

Published at DZone with permission of Horatiu Dan. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Distribution Design Patterns in Java - Data Transfer Object (DTO) And Remote Facade Design Patterns
  • Writing DTOs With Java8, Lombok, and Java14+
  • Redefining Java Object Equality
  • Dynamic File Upload Component in Salesforce LWC

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: