Skip to content

DTO vs VO: Why We Still Need to Distinguish Between Them

Why Do We Still Try to Draw a Distinction?

Makonea·Apr 21, 2026·44 min

Before We Begin

The distinction between DTO (Data Transfer Object) and VO (Value Object) is not simply a matter of coding conventions.

It is something like a living fossil that records how the paradigms of software architecture have evolved over time.

Tracing the history of this debate is equivalent to understanding the intellectual thread that runs from the era of "distributed objects" through the era of "Domain-Driven Design (DDD)" and finally into the modern era shaped by "functional programming (FP)",

a continuous flow of technical thought that connects each of those periods.

Both appear similar in that they are "value-centered objects," but they diverge based on their design purpose and design approach.

In general, a DTO is concerned with the shape of data, while a VO is concerned with the meaning of data. With that in mind, let's trace what DTOs and VOs actually are.

Before diving in, I'll include a bit of the foundational knowledge you'll need.

Since a brief explanation of the Domain Model and Business Logic will make things easier for readers, here is that explanation.

Prerequisites

1. Domain Model

A Domain Model is a collection of objects that represent a specific problem area (domain).

It is an abstraction of real-world concepts implemented in code.

For example, in a music streaming service, concepts such as "Album," "Artist," and "Track" might be part of the Domain Model.

Characteristics of a Domain Model

Object-oriented: composed of classes and objects.

Example: Album, Artist, Track classes

Relationship-centric: domain objects have mutual relationships with one another.

Example: one Album can have multiple Tracks, and each Track can be associated with multiple Artists.

Reflects business rules: the Domain Model reflects the core rules and constraints of the domain.

Example: "An album must have at least one track."

2. Business Logic

Business Logic refers to the rules and procedures required to handle the problems the software is meant to solve.

It operates on top of the Domain Model and implements real-world business requirements.

For example, a rule such as "artist information must be present when creating an album" belongs to Business Logic.

Characteristics of Business Logic

Domain Model-based: Business Logic is implemented by leveraging the Domain Model.

Core functionality: defines the essential behavior of the application.

Example: "Create an album, add tracks, and link artists."

Flexibility: when business requirements change, the Business Logic must also be updated.

The Domain Model represents real-world concepts as objects, while Business Logic is the set of rules that processes actual business requirements on top of the Domain Model.

The Domain Model is a broader, higher-level concept encompassing data and state, whereas Business Logic refers to the concrete implementation that performs operations using that data.

3. Entity

An Entity is an object that has a unique identifier within a specific domain and maintains its identity over time.

It models a real-world object, storing and managing data and state.

For example, a "User" or a "Product" can be represented as an Entity.

Core Characteristics of an Entity

Unique Identifier (ID):

Each Entity has a unique identifier that distinguishes it from other objects.

Example: userId in the User class, productId in the Product class.

Persistence:

Entities are stored in a database or file system and maintain their state even after the application shuts down.

Identity:

If two Entities share the same ID, they are considered the same object even if their data differs.

Example: if User objects A and B share the same userId, A and B refer to the same user.

State Change:

An Entity's state can change over time, but its identifier does not change.

Example: even if a user changes their name, the userId remains the same.

The Birth of Two Giants (2002–2003)

데이터 전송 객체 (DTO)

An object that carries data between processes in order to reduce the number of method calls.

When you’re working with a remote interface, such as Remote Facade (388), each call to it is expensive. As a result you need to reduce the number of calls, and that means that you need to transfer more data with each call. One way to do this is to use lots of parameters. However, this is often awkward to program; indeed, it’s often impossible with languages such as Java that return only a single value.

The solution is to create a Data Transfer Object that can hold all the data for the call. It needs to be serializable to go across the connection. Usually an assembler is used on the server side to transfer data between the DTO and any domain objects.

Many people in the Sun community use the term “Value Object” for this pattern. I use it to mean something else. See the discussion on page 487.

The Birth of the DTO (Data Transfer Object)

The DTO was introduced and widely popularized through Martin Fowler's Patterns of Enterprise Application Architecture.

The core idea was as follows.

"To minimize remote calls, pack all the data that would otherwise require multiple calls into a single dumb object and send it in just one call."

At that time, the DTO was a mutable object, and the common explanation of DTOs as mutable and VOs as immutable originates from this context.

So why did this discussion arise?

In the early 2000s, distributed component technologies such as EJB (Enterprise JavaBeans) were mainstream. What was the greatest technical constraint of that era?

It was the cost of network calls. The cost of remote calls between server and client, and between servers, was far higher than it is today.

To retrieve information from a remote object (via a remote interface, since it was a distributed component system), you had to pay that call cost, which was considerable, and the DTO was the object designed to reduce it.

*What is a Distributed Component System?: An architecture in which multiple computers serve as nodes, independent components are distributed across them, and those components operate together as an integrated system.

At that time, a DTO existed solely for the purpose of data transfer. Let's look at the example Martin Fowler provided.

Before getting into the code, here is what it aims to accomplish.

Extract the necessary data from the Domain Model, convert it into a simple structure, make it serializable, and use it for remote calls.

It also shows an example of converting data received from a client back into a Domain Model.

Domain Model: Objects that represent the core business logic and data of the application (e.g., Album, Track, Artist classes).

These classes have complex relationships and methods, making them unsuitable for remote calls.

Data Transfer Object (DTO): An object that simplifies and converts Domain Model data into a serializable form (e.g., AlbumDTO, TrackDTO).

Assembler: The Assembler is responsible for converting between Domain Models and DTOs (e.g., the AlbumAssembler class). This class converts domain objects into DTOs or DTOs into domain objects. In modern architectures, the Assembler is rarely used and is typically replaced by a Mapper. I plan to write about why this shift occurred if time permits.

Since concepts alone can be hard to grasp, let's look directly at the code Fowler presented in P of EAA.

To understand the Domain Model through the examples in Martin Fowler's Patterns of Enterprise Application Architecture (P of EAA), take a look at the code blocks below.

(1) Writing the DTO (Domain → DTO)

class AlbumAssembler {
    public AlbumDTO writeDTO(Album subject) {
        AlbumDTO result = new AlbumDTO();
        result.setTitle(subject.getTitle());           // Set album title
        result.setArtist(subject.getArtist().getName()); // Set artist name
        writeTracks(result, subject);                  // Convert track list
        return result;
    }

    private void writeTracks(AlbumDTO result, Album subject) {
        List newTracks = new ArrayList();
        Iterator it = subject.getTracks().iterator();
        while (it.hasNext()) {
            TrackDTO newDTO = new TrackDTO();
            Track thisTrack = (Track) it.next();
            newDTO.setTitle(thisTrack.getTitle());     // Set track title
            writePerformers(newDTO, thisTrack);        // Convert performer list
            newTracks.add(newDTO);
        }
        // Convert to TrackDTO array and store in album DTO
        result.setTracks((TrackDTO[]) newTracks.toArray(new TrackDTO[0]));
    }

    private void writePerformers(TrackDTO dto, Track subject) {
        List result = new ArrayList();
        Iterator it = subject.getPerformers().iterator();
        while (it.hasNext()) {
            Artist each = (Artist) it.next();
            result.add(each.getName());                // Extract performer names
        }
        // Convert performer names to string array and store in track DTO
        dto.setPerformers((String[]) result.toArray(new String[0]));
    }
}

The code breaks down as follows.

writeDTO: Converts an Album object into an AlbumDTO.

Sets the album title (title) and artist name (artist) on the DTO.

Track information is handled via the writeTracks method.

writeTracks: Converts the tracks contained in an album into an array of TrackDTOs.

Extracts the title and performer information for each track and stores them in the DTO.

writePerformers: Converts a track's performer names into a string array.

(2) Creating a Domain Object from a DTO (DTO → Domain)

class AlbumAssembler {
    public void createAlbum(String id, AlbumDTO source) {
        // Look up the artist in the registry
        Artist artist = Registry.findArtistNamed(source.getArtist());
        if (artist == null)
            throw new RuntimeException("Artist not found: " + source.getArtist());

        // Create a new album domain object from the DTO's title and artist
        Album album = new Album(source.getTitle(), artist);
        createTracks(source.getTracks(), album);
        Registry.addAlbum(id, album);
    }

    private void createTracks(TrackDTO[] tracks, Album album) {
        for (int i = 0; i < tracks.length; i++) {
            // Create a new track object from the DTO's track title and add it to the album
            Track newTrack = new Track(tracks[i].getTitle());
            album.addTrack(newTrack);
            createPerformers(newTrack, tracks[i].getPerformers());
        }
    }

    private void createPerformers(Track newTrack, String[] performerArray) {
        for (int i = 0; i < performerArray.length; i++) {
            // Look up the performer in the registry and associate it with the track
            Artist performer = Registry.findArtistNamed(performerArray[i]);
            if (performer == null)
                throw new RuntimeException("Artist not found: " + performerArray[i]);
            newTrack.addPerformer(performer);
        }
    }
}

The code breaks down as follows.

createAlbum: Creates a new album object based on the DTO.

Looks up artist and track information in the Registry and links them.

createTracks: Creates new track objects based on the track information in the DTO.

createPerformers: Links artist objects based on the performer information of a track.

(3) Updating a Domain Object Using a DTO

class AlbumAssembler {
    public void updateAlbum(String id, AlbumDTO source) {
        // Look up the existing album in the registry
        Album current = Registry.findAlbum(id);
        if (current == null)
            throw new RuntimeException("Album not found: " + source.getTitle());

        // Update only if the title has changed
        if (!source.getTitle().equals(current.getTitle()))
            current.setTitle(source.getTitle());

        // If the artist has changed, look up the new artist in the registry and replace it
        if (!source.getArtist().equals(current.getArtist().getName())) {
            Artist artist = Registry.findArtistNamed(source.getArtist());
            if (artist == null)
                throw new RuntimeException("Artist not found: " + source.getArtist());
            current.setArtist(artist);
        }

        updateTracks(source, current);
    }

    private void updateTracks(AlbumDTO source, Album current) {
        for (int i = 0; i < source.getTracks().length; i++) {
            // Update the track title
            current.getTrack(i).setTitle(source.getTrackDTO(i).getTitle());
            // Clear the existing performer list and reset it based on the DTO
            current.getTrack(i).clearPerformers();
            createPerformers(current.getTrack(i), source.getTrackDTO(i).getPerformers());
        }
    }
}

The code breaks down as follows.

updateAlbum: Updates an existing album object with information from the DTO.

Compares the title, artist, and track information to apply any changes.

updateTracks: Updates track information or adds new tracks.

The Birth of the VO (Value Object): Eric Evans's DDD (2003)

VALUE OBJECTS

Many objects have no conceptual identity. These objects describe some characteristic of a thing.

If a child is drawing, he cares about the color of the marker he chooses. He may care about the sharpness of the tip. But if there are two markers of the same color and shape, he won't care which he uses. If a marker is lost and replaced by another of the same color from a new pack, he can resume his work unconcerned about the switch.

Ask the child about the various drawings on the refrigerator and he will quickly distinguish those he made from those his sister made. He and his sister have useful identities, as do their complete drawings. But imagine how complicated it would be if he had to track which lines in a drawing were made by each marker. Drawing would no longer be child's play.

Since the most conspicuous objects in a model are usually ENTITIES, and it is so important to track their identity, it is natural to start thinking about assigning an identity to all domain objects. Sometimes frameworks are created to assign every object a unique ID. Extra analytical effort goes into working out foolproof ways of tracking objects across distributed systems and in database storage.

This can be costly in terms of system performance, as the system has to cope with all that tracking, and many possible performance optimizations are ruled out. Equally important, it muddles the model, forcing all objects into the same mold, and tacking on misleading artificial identities.

**Tracking the identity of ENTITIES is essential, but attaching identity to other objects can hurt system performance, add analytical work, and muddle the model by making all objects look the same.**

**Software design is a constant battle with complexity. We must make distinctions so that any special**

As programming grew increasingly complex,

Business Logic grew complex as well, and it was in the pursuit of expressing that logic "elegantly" that the VO (Value Object) emerged.

This concept of the VO was introduced and widely spread through the masterwork Domain-Driven Design, commonly known as DDD,

"Encapsulate a value and its associated Business Logic into a single Object; compare equality between these objects based on the value they hold rather than an ID; and once created, the object must be immutable (Immutable)."

(VO example from the DDD book)

(1) Address Value Object

public class Address {
    private final String street; // Street
    private final String city;   // City
    private final String state;  // State/Region

    // Constructor - initializes all fields at once (immutable object, so no changes allowed afterward)
    public Address(String street, String city, String state) {
        this.street = street;
        this.city = city;
        this.state = state;
    }

    // Getter - allows read-only access
    public String getStreet() { return street; }
    public String getCity()   { return city; }
    public String getState()  { return state; }

    // `equals` - compares equality based on value, not ID (the core characteristic of a VO)
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return Objects.equals(street, address.street) &&
               Objects.equals(city, address.city) &&
               Objects.equals(state, address.state);
    }

    // `hashCode` - always overridden alongside `equals` (ensures consistency when used in collections)
    @Override
    public int hashCode() {
        return Objects.hash(street, city, state);
    }

    @Override
    public String toString() {
        return "Address{" +
               "street='" + street + '\'' +
               ", city='" + city + '\'' +
               ", state='" + state + '\'' +
               '}';
    }
}

(Code implementing a VO prior to Java 14. While using record is considered safer in Java 14 and later, this example is included to illustrate how things changed over time.)

Address is a Value Object that holds address information, including attributes such as street, city, and state.

A Value Object is generally an immutable object, and two instances with the same values are considered the same object.

In the early days, the roles of the two concepts were clear-cut.

The Old DTO vs. VO Distinction

Item

DTO

VO

Purpose

Network transfer

Domain Model representation

Structure

Mutable object (get; set;)

Immutable object (final, val, readonly)

Logic

None

Includes validation, equality, etc.

Location

API boundary

Domain layer

As the table shows, the distinction between the two concepts was clear during this period.

DTOs prioritized ease of serialization, VOs prioritized meaningful representation,

DTOs were a box that held all the data needed for remote communication,

and VOs were values that carried meaning within domain design.

However, as hardware performance improved, this boundary began to erode.

Why Did the DTO vs. VO Debate Begin?

(Java Spring)

After the era of distributed systems and EJB, the powerful Spring framework emerged.

The differences between the distributed systems and EJB era and the Spring framework era could be illustrated endlessly,

but let's use a simple analogy.

In the old days, houses were poorly equipped and had no kitchen inside (the EJB era).

The bedroom was inside the house,

but the kitchen, bathroom, and other rooms were outside (on separate servers), so to have breakfast you had to leave the house and go to the outdoor kitchen,

and going to the bathroom also required making an external call (a network call).

To move freely between the outdoor kitchen and bathroom, you had to carry all your household items with you (toilet paper, a knife and fork for the meal, and so on).

But then powerful web frameworks led by Spring arrived,

the boundaries of distributed systems disappeared, and passing data between layers within a single application

(Controller ↔ Service ↔ Repository) became the primary concern.

In other words, now that all the rooms were inside the same house, the expensive network call cost disappeared,

and DTOs began to be used not as objects for remote communication but as objects for passing data between layers.

"Since we're all under the same roof anyway, do we really need to carry around all our household items (DTO)? Can't we just grab the thing we need at the moment (VO)?"

As a result, the distinction between DTO as a transfer object and VO as a domain modeling object began to blur, and the question of why DTOs had to be used at all started to gain traction.

Has the DTO Become Unnecessary?

(Clean Code, Chapter 8: Boundaries)

To state the conclusion first: the role of the DTO began to evolve in a somewhat different direction.

As Uncle Bob stated in his classic Clean Code, "a system must establish clear boundaries with external code"

is the principle at play here.

It is in this role, as a boundary object that separates external code from internal code, that the DTO came to be used.

DTOs are used at the boundaries with the outside world, such as APIs, databases, message queues, and files,

while VOs are used within internal models.

The DTO began to function as a Boundary Adapter that wraps external APIs.

In other words, the DTO's role became that of a translation tool for communicating with the outside world,

while the VO became the language itself within the internal domain.

On top of that, as multi-threading became mainstream, a new shift occurred within DTOs as well.

(Effective Java, Item 17: Minimize Mutability)

In the early 2010s, when multi-threading once again became a central topic, the functional programming principle of data immutability was adopted, and immutability began to establish itself as a new design principle.

Following this trend, DTOs also moved away from being mutable objects, adopting constructs like the record keyword and transitioning into immutable objects.

(Later, in Clean Architecture, Robert Martin emphasized through the concept of the UseCase Output DTO that a DTO should be defined as a one-way interface between the external world and the Use Case layer. In other words, DTOs should always exist "outside the boundary," while domain models should exist only within it. However, since this topic falls outside the main argument of this post, it is enough simply to know that such a discussion exists.)

And as both DTO and VO came to treat immutability as their default, the difference between the two became even more subtle,

and the current debate has shifted to immutable object without domain logic (DTO) vs. immutable object with domain logic (VO).

The Current DTO vs. VO Distinction

Item

DTO

VO

Immutability

Recommended (semi-required)

Required

Includes Domain Logic

No (simple container)

Yes (domain logic such as equality and validation)

Value-based Equality

ID/key-based or not applicable

equals and value-based identity required

Location

API/input-output layer

Domain layer

Dependencies

Framework-based (Jackson, gRPC, etc.)

Independent, meaning-centric

Of course, this does not mean that DTOs never contain logic.

In practice, DTOs sometimes include data validation logic. The "no logic" claim should therefore be understood to mean, more precisely, "does not contain domain logic."

Closing Thoughts...

(Martin Fowler)

The fundamental horror of this anti-pattern is that it's socontrary to the basic idea of object-oriented design.

-Martin Fowler

Given that the differences are gradually fading, why do we still bother distinguishing between the two? Can't we just use DTOs everywhere?

The reason goes beyond technical differences: it is an effort to express design intent clearly at the code level.

Using a DTO is a declaration of a boundary of responsibility: "This object's only job is to transfer data from layer A to layer B. It knows nothing about the meaning of the data or any business rules."

Using a VO is a declaration of encapsulated domain knowledge: "This object carries complete meaning as a 'value' in itself, and it takes direct responsibility for validating its own value and handling any related behavior."

Martin Fowler is the person who popularized the concept of the DTO, yet he simultaneously argues against the use of data-only objects like DTOs, such as the J2EE model, at the core of domain logic.

Relying on DTOs alone is an anti-pattern, and VOs must also exist because a VO does not merely hold a value; it encapsulates the value together with the behavior associated with it.

But what if only VOs existed? You would have to expose complex Business Logic directly to the outside world, which is to say you would break the boundaries between layers, allowing external requests to intrude upon and alter the domain layer. Put differently, if domain objects were made to shoulder serialization and API contracts as well, the domain design would become corrupted.

In the end, there is no perfect answer on either side.

That is why we continue to tug at the rope between DTO and VO. And unless the day comes when that tension disappears, this dilemma will persist.