CodeGym /Courses /Java Server /Typical DTO parts

Typical DTO parts

Java Server
Level 11, Lesson 2
Available

1. DTO as a “questionnaire” at the application boundary

If you look at a DTO from a high level, it is very similar to paper forms in real life. When you fill out an application, it almost always has a “request number,” a “review status,” a “comment (optional),” and sometimes a “list of attachments + count.” This is not because bureaucracy enjoys suffering (though... let us not argue about that), but because the boundary between systems requires structure.

DTOs in the backend world perform the same role. They fix the shape of the data so that the two sides — the client and the server — do not have to guess “how should I understand this field.” That is why DTOs get typical blocks: an identifier lets you refer to a specific object, a status gives a finite set of states, optional fields add flexibility without breaking the scenario, and a list wrapper makes the response self-contained and extensible.

In ReadLater Starter, this will be especially visible: we have local reading list objects (they will have their own id), and we also have an external book catalog (it will have externalId). A book has a lifecycle (planning to read → reading → finished), so a status will appear. A comment is useful, but not always needed, so it will be optional. And we want to return the list of books not just as an array, but together with count, so the client has an easier time.

2. Identifiers: id and externalId

An identifier in an API is the object’s “name,” by which the client can say: “show me this exact item.” Without an identifier, everything turns into “show me that book, well... the one that was third in the list yesterday, with the green cover, you know.” Unfortunately, computers understand badly. Especially green covers.

Two worlds of identifiers

In ReadLater Starter, we will get two types of identifiers almost immediately, and that is normal:

1) Local id (usually a number, long).
This is the identifier of an item in our reading list. It is created and controlled by our application. The client will use it to get a specific item, update it, or delete it.

2) External externalId (usually a string).
This is the identifier of a book in an external catalog. We did not invent it, and the rules there are not ours either. We can store it as a link to the source of truth, but we cannot “make it prettier” and should not use it in place of our local id.

A very practical analogy: local id is like the receipt number at your app’s cloakroom. externalId is like the product number in a store catalog. You can write the product number on the receipt to make searching faster later, but that does not make the receipt number the product number.

id in a create request: usually not needed

One of the most common beginner mistakes is to automatically shove id into every request DTO “because the object has an id.” But in a creation (create) scenario, id is usually generated by the server. The client cannot know the id yet, because the object does not exist yet.

Let us compare two DTOs (simplified, without JSON mapping details, because right now we are only talking about meaning):

// Request DTO for creation: there is NO id here, because it does not exist yet (the server will provide it).
public record CreateReadingItemRequest(
        String title,      // Semantic data that the client can really send
        String author,     // Author is part of the "content", not the identity
        String externalId, // Link to an external catalog (if there is one)
        String comment     // User comment (may be absent)
) { }
// Response DTO: here id already exists, because the server assigned it and must tell the client.
public record ReadingItemResponse(
        long id,           // Local identifier of the object inside our API
        String title,
        String author,
        String externalId, // External identifier — not a replacement for id, but a "link outward"
        String comment
) { }

Notice: CreateReadingItemRequest does not contain id, while ReadingItemResponse does. This is not “because that is the convention,” but because the meaning is different: the request describes input for creation, while the response is the result, where the server must already report which identifier it assigned.

id in a request: rare cases

Sometimes it does show up. For example, if the client generates the identifier itself (rare in ordinary CRUD APIs) or if the request describes updating a resource and the identifier goes in the URL. But in our course, we will stick to the simple and common option: the resource identifier for update/delete is part of the path (/reading-list/{id}), not a field in the body. You already discussed this earlier in the topic “where to put data: path/query/body.”

If you want to feel the difference “by hand,” here is the same meaning in two forms:

Option A (more typical for HTTP):

PUT /api/v1/reading-list/10
body: { "title": "...", "author": "...", ... }

Option B (often worse, but it happens):

PUT /api/v1/reading-list
body: { "id": 10, "title": "...", ... }

We are not making a religion out of this, but for a learning project, Option A is simpler and more disciplined: the identifier is in the path, the data is in the body.

Mini table: where the identifier lives

To avoid confusion, it is useful to fix a simple rule in table form:

Scenario What the client does Where we expect the identifier
Create item “Create a new one” In the response (id appears after creation)
Get item “Show me a specific one” In the URL/path (/reading-list/{id})
Update/delete “Change/delete a specific one” In the URL/path (/reading-list/{id})
Link to an external catalog “Associate with a book from the catalog” In the externalId field (usually in request/response)

And one especially important thought: id and externalId do not compete; they belong to different “universes.” Inside our API, the main one is id. For connection to the external world, it is externalId.

3. Status field: when string is the enemy and enum is the friend

A status in a DTO is a way to say: “the object has a finite number of states, and we are not going to accept any random strings the client invents.” If you do not limit the status, you will very quickly get a zoo of values: "planned", "PLANNED", "planed" (yes, with one n), "in progress", "INPROGRESS", "almost finished, honestly"... And then you either write endless ifs, or you start dreaming of a job as a gardener (statuses are simpler there: “growing” / “not growing”).

For ReadLater, status is practically the heart of the domain: a book is usually planned, in progress, or finished. So status is an ideal candidate for an enum-like field.

Minimal status enum

// Enum fixes the allowed states: you cannot just put "anything" in here anymore.
public enum ReadingStatus {
    PLANNED,     // Planning to read
    IN_PROGRESS, // Reading right now
    FINISHED     // Read
}

In JSON, this will usually look like the strings "PLANNED", "IN_PROGRESS", "FINISHED". Yes, they are strings, but the important part is the meaning: the strings are not arbitrary, but come from a fixed set.

Status in DTOs

In a request DTO, the status may be required or have a default value (for example, if nothing is sent, we assume PLANNED). But we are not going deep into validation and defaults today. What matters is this: a status is not “any text,” but a contract.

// In a request, the status is also part of the contract: the client chooses from a fixed set.
public record CreateReadingItemRequest(
        String title,
        String author,
        ReadingStatus status, // Not String: so we do not multiply "planed"/"planned"/"PLANNED"
        String externalId,
        String comment
) { }

And the response DTO also shows the status explicitly:

// In the response, we return the status back: the client needs to see the current stage.
public record ReadingItemResponse(
        long id,
        String title,
        String author,
        ReadingStatus status, // The same enum contract as on input
        String externalId,
        String comment
) { }

For a beginner, this may look like “why not just make String status? It would be easier.” Yes, easier — right up to the first bug. enum does two useful things: it documents the allowed values right in the code and helps the compiler catch mistakes (for example, you will not accidentally assign the status "FINISED" — the compiler will not allow it).

Status ≠ a life comment

Another common distortion is trying to use status as a “universal meaning field”: stage, tag, priority, mood, and weather outside all at once. In our project, status is specifically the reading stage. If you want to store “I want to read this in summer at the dacha,” that is already comment, meaning a separate field and different semantics.

You can even draw a very simple “life cycle” diagram for the status, so the brain stops treating it like arbitrary text:

The arrow direction is the expected sequence of states, not "any transition anywhere."
PLANNED  ->  IN_PROGRESS  ->  FINISHED

No philosophy. Just a contract: the book has a clear state.

4. Optional fields: different worlds

Optional fields are where the most subtle (and most common) mistakes begin. Because the brain wants simplicity: “well, if it was not sent, then it is empty.” But the contract may say: “not sent” and “sent as empty” are different stories.

In ReadLater, two typical optional fields are externalId and comment. A user may not have an external identifier (they added a book manually, without a catalog). And a user may not have a comment (they just added the book and went on living life, instead of writing an essay about “why I will read this in April”).

Three states worth distinguishing

Let us take comment. From the JSON contract perspective, you effectively have three different situations:

1) The field is missing entirely (missing field).
The client did not send the field. This may mean “I do not know,” “I do not want to touch it,” or “I do not support it.” In update scenarios this is especially important, but even in create it makes sense as “I am not specifying it.”

{
  "title": "Clean Code",
  "author": "Robert C. Martin",
  "status": "PLANNED"
}

2) The field exists, but is null.
The client explicitly said: “the value is absent.” Sometimes this means “clear the field” (again, especially relevant in partial updates).

{
  "title": "Clean Code",
  "author": "Robert C. Martin",
  "status": "PLANNED",
  "comment": null
}

3) The field exists, but the string is empty.
The client sent a value, but it is empty. This is not “no value”; it is “there is a value, but it is empty.” Sometimes this is an error, sometimes acceptable, sometimes it needs normalization. The main thing is that this is a third state, not equal to the previous two.

{
  "title": "Clean Code",
  "author": "Robert C. Martin",
  "status": "PLANNED",
  "comment": ""
}

The important thing here is not to jump to a conclusion too early. At the JSON contract level, these really are three different situations. But a plain Java field String comment by itself usually distinguishes only “has a value” and “no value”; it does not separately store the fact that “the field was missing” versus “the field came as null.” So first we fix the meaning at the contract level, and only then decide how exactly to support that meaning during parsing and binding.

We are not deciding now which of the three states is “more correct” — that depends on the contract rules. But today it is important to understand something else: if you do not agree on this, you will get chaos. And the chaos will appear not because JSON is bad, but because the contract is undefined.

Optional in Java DTOs

At the Java level, beginners usually ask: “What should we do with optional fields? Optional<String>? String and null?”

In educational and practical code, DTOs most often use plain String, allowing null. This is not the ideal world of dreams, but it is an honest and understandable model: a field can either be a string or be absent. Then the application layers decide what absence means.

// For DTOs, ordinary nullable fields are often enough: it is a simple and honest contract model.
public record CreateReadingItemRequest(
        String title,
        String author,
        ReadingStatus status,
        String externalId, // may be null if the book was added without an external catalog
        String comment     // may be null if the user did not leave a note
) { }

And it is even useful to show that this is “not scary” as long as you keep the contract in mind:

// Example: creating a request with missing optional fields.
CreateReadingItemRequest req = new CreateReadingItemRequest(
        "Clean Code",
        "Robert C. Martin",
        ReadingStatus.PLANNED,
        null, // externalId is absent
        null  // comment is absent
);

// Important: null here is not "an error by itself", but a possible field state by contract.
System.out.println(req.externalId()); // null
System.out.println(req.comment());    // null

The only thing to avoid is turning null into a “universal mush,” where null means at the same time “not sent,” “delete the value,” “value unknown,” and “I am too lazy to think.” That is exactly why the contract exists: it should describe what we expect.

“Optional” does not mean “junk drawer”

There is another typical trap: when a field is optional, the developer starts treating it as a place for everything. “Oh, we have comment, let us put tags, rating, and purchase date there too.” That becomes painful to untangle later.

An optional field is not a “pocket for future features.” It should have one clear meaning, it just may be absent. In our project, comment is the user’s comment, no more and no less. And externalId is a link to the external catalog, if there is one.

5. List response wrapper: items + count

When you return a list of objects in an API, the first instinct is to return a plain array: [{...}, {...}]. It is quick, and in educational examples this is often done. But in practical APIs it becomes clear almost immediately that an array alone is not enough: you also want to return extra information nearby.

In our ReadLater, we need at least count. Someone may say: “But count can be calculated as items.length!” True. But returning count is still often useful: it makes the response self-contained and also leaves room to extend the contract without breaking clients.

A naked array and its problem

If we returned the reading list “as is,” the response could look like this:

[
  { "id": 1, "title": "Clean Code", "status": "PLANNED" },
  { "id": 2, "title": "Effective Java", "status": "IN_PROGRESS" }
]

The problem is not that this is “not REST.” The problem is that as soon as you want to add count, you have to change the response shape:

{
  "items": [ ... ],
  "count": 2
}

And that is already a contract change, and the client that expected an array may break (or start writing hacks: “if an array comes, handle it this way; if an object comes, handle it another way”).

The wrapper makes the contract extensible

That is why in a learning project (and in the real world), a wrapper is often a good investment. In ReadLater Starter, we choose the shape in advance:

{
  "items": [
    {
      "id": 1,
      "title": "Clean Code",
      "author": "Robert C. Martin",
      "status": "PLANNED",
      "externalId": "OL12345M",
      "comment": "Find a paper edition"
    }
  ],
  "count": 1
}

It is readable, predictable, and easy to extend. Today it is count, tomorrow (in theory) you can add something else — but we will not run ahead. The important thing is that the contract already has a “place” for metadata.

List response DTO in Java

At the Java DTO level, this usually looks like this:

import java.util.List;

// List response DTO: items are the elements themselves, count is metadata (the quantity).
public record ReadingListResponse(
        List<ReadingItemResponse> items, // List contents
        int count                        // Number of elements (convenient for the client and for contract extension)
) { }

And here is an example “just so the brain can see the shape” (this is not about the server, just about the structure):

import java.util.List;

// One reading list item (as it looks in the response).
List<ReadingItemResponse> items = List.of(
        new ReadingItemResponse(1L, "Clean Code", "Robert C. Martin",
                ReadingStatus.PLANNED, "OL12345M", null) // comment is absent — that is normal
);

// count usually matches items.size(), but what matters is that the field exists in the contract.
ReadingListResponse response = new ReadingListResponse(items, items.size());

System.out.println(response.count()); // 1

Notice that count here is equal to items.size(). That is normal. What matters is not a “magic number,” but the contract: the client knows in advance that count exists and that it is the number of items.

An empty list is not an error

When you return a list, an empty list is a normal successful response. This is a very “adult” habit: if the list is empty, that is not a 404, but a 200 with items: [] and count: 0. And here the wrapper is also nice: the response shape is the same for both the empty and non-empty cases.

{
  "items": [],
  "count": 0
}

The client does not have to guess what the server decided to return today. The contract remains stable.

6. Mini DTO assembly for ReadLater

Now let us carefully assemble the picture in our head without turning the lecture into “designing all the DTOs for the project” (that will come later in the day’s plan). What matters is to see how the four typical parts — id, status, optional fields, and list wrapper — combine into one small contour.

In ReadLater, one reading list item in the response will almost inevitably contain id (otherwise the client cannot address it again), will contain status (otherwise it is unclear what stage it is at), and will contain optional fields (externalId, comment), because a catalog link and user notes are not always present.

A list of items, in turn, is logically better returned in a wrapper: items + count. And this is not “extra classes for the sake of classes,” but a way to make the contract stable: one item — one shape, a list — another shape, and the list has a place for metadata.

To pin down this “assembly” visually, here is a compact table (not as a memorization list, but as a map):

DTO (roughly) id status optional fields list wrapper
Create request no yes yes no
Single item response yes yes yes no
List response inside items inside items inside items yes (items, count)

This table does not dictate the “only correct option.” It shows a repeating logic: a create request usually does not require id, a response usually contains id, status is a fixed set of values, optional fields must really be optional in meaning, and a list is often more convenient to return as an object with metadata.

7. Typical mistakes when designing DTO parts

Mistake No. 1: mixing id and externalId as if they were the same thing.
Usually this happens like this: a student sees that there is an external catalog and decides, “well, there is already an identifier, why do we need another one.” And then it turns out that the reading list lives its own life, and internal operations (update, delete) must refer to the internal object, not the external catalog. externalId is a link outward, while id is the identity of the object inside our API.

Mistake No. 2: requiring id in a create request “because you cannot do without it.”
This breaks the meaning of creation: if the client has to send id, then who is responsible for uniqueness and generation? In a simple CRUD API, the server usually provides the id, and that makes life much easier for everyone. The client sends semantic data (title, author, status), and the server returns the result and the assigned identifier.

Mistake No. 3: making status a free-form string and hoping that “everyone will write it correctly.”
Hope is a wonderful feeling, but it does not work in a contract. The status field needs to be limited to a set of values. Even if you store it as a string in JSON, the meaning must be enum-like: fixed variants, clear and documented. Otherwise you will quickly get a dozen incompatible spellings of the same status.

Mistake No. 4: declaring a field “optional” but not agreeing on what null means and what missing field means.
Then some clients will start sending null, some will not send the field at all, some will send an empty string, and each side will think that their own approach is the “logical” one. In the end, the server has to guess. The contract must define the meaning ahead of time, otherwise optionality becomes a source of bugs.

Mistake No. 5: returning a list as a naked array and then suddenly wanting to add metadata.
Today you returned [], tomorrow you wanted to return count, the day after tomorrow something else, and now you have a changed response shape, meaning a contract change. A response wrapper ({ "items": [...], "count": N }) often saves you from this problem in advance, especially in a learning project where stability and predictability matter.

1
Task
Java Server, level 11, lesson 2
Locked
Status and optional fields in request DTO
Status and optional fields in request DTO
1
Task
Java Server, level 11, lesson 2
Locked
Wrapper list with items and count
Wrapper list with items and count
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION