Skip to content

DTOs & Mapping : The Good, The Bad, And The Excessive

Sponsor: Using RabbitMQ or Azure Service Bus in your .NET systems? Well, you could just use their SDKs and roll your own serialization, routing, outbox, retries, and telemetry. I mean, seriously, how hard could it be?

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.


Something that’s often overused, misunderstood, or just incorrectly applied in software development: Data Transfer Objects (DTOs). DTOs are a common pattern, but like many things in software, they tend to get used in places where they don’t actually solve the problem at hand. So, what are DTOs really for? When should you use them? And when are they just extra work with no real benefit? Let’s dig in.

YouTube

Check out my YouTube channel, where I post all kinds of content accompanying my posts, including this video showing everything in this post.

What Are Data Transfer Objects (DTOs)?

First, let’s start with a basic understanding of what DTOs are. At their core, DTOs are objects used to transfer data between different layers or tiers of an application. They’re often used to map data from one representation to another—especially when crossing boundaries like between your database and your UI or API layer.

However, confusion often creeps in around the term “entity.” When you say “entity,” people might mean different things. Some think about entities in the context of an Object-Relational Mapper (ORM) like Entity Framework in .NET. In this case, an entity is simply a data model mapping to a database table. Others think about entities in the Domain-Driven Design (DDD) sense, where an entity represents a concept with behavior and identity within the domain.

This misunderstanding is the first place where things go wrong with DTOs. Because of the ambiguity around what an entity really means, people often feel the need to create DTOs to avoid exposing their underlying data model or domain model directly to other parts of the system or external consumers.

Why Do People Introduce DTOs?

The most common reason developers create DTOs is because they don’t want to expose their underlying data model or domain model directly. For example, if you have an ORM entity that maps directly to your database schema, you might not want to send that exact entity out through your API or to another layer in your application. Instead, you create a DTO that acts as a translation or mapping of that entity, which you then send over the wire or pass along in memory.

This reasoning is valid in some contexts, but here’s where I want to push back a bit and play Devil’s Advocate. Why can’t you just return the underlying data model or entity? Why is the automatic assumption that you must create a DTO?

Entity Services and HTTP APIs: The Root of Some DTO Dogma

A great illustration of this dogma comes from how people build HTTP APIs. Often, people create DTOs that are almost one-to-one mappings of their entities. The API’s endpoints map CRUD operations directly to the database tables. For example:

  • GET to retrieve a shipment
  • POST to create a shipment
  • PUT to update a shipment
  • PATCH for partial updates
  • DELETE to remove a shipment

This approach treats your API as just a direct mapping of HTTP methods to database CRUD operations. There’s nothing inherently wrong with this, but it forces you down a certain path. You end up thinking about your API as just a thin layer over your database schema.

But your API doesn’t have to be that way. The resources exposed by your API don’t have to be one-to-one with database records. For example, in a server-side rendered application, you’d do things very differently. You’d gather and compose data from multiple sources to produce a full HTML view, not just raw data from a single table.

Let me illustrate this with a shipment example. If you’re returning a shipment entity as your DTO, you might only send back the shipment ID, start date, and status.

But if you’re composing a richer view for server-side rendering, you’d also include customer information like the customer’s name, which might not be directly on the shipment entity. You’d also include possible actions the user can take, such as “arrive,” “cancel,” or “schedule pickup.”

When you’re thinking purely in terms of entity services and database records, you miss out on this richer composition. You end up with DTOs that are just thin shells around your database tables, not much different from the entities themselves.

The Real Value of DTOs: Managing Coupling and Composition

In my opinion, there are two primary benefits to using DTOs:

  1. Managing Coupling: DTOs help control how tightly your external layers or consumers are coupled to your internal data models.
  2. Composition: DTOs enable you to compose data from multiple sources or models into a single, convenient shape optimized for a particular use case or consumer.

If you’re not doing any composition and your DTOs are almost identical to your database records, then you’re probably just creating unnecessary work. Every time you change your underlying data model, you’re forced to update multiple DTOs across layers, which adds complexity and maintenance overhead without providing real value.

On the other hand, if you’re managing coupling well and controlling how your internal models are exposed, DTOs become very useful. They act as a protective layer that lets you evolve your internal data models without breaking external consumers. But if you don’t have this problem yet—if you only have a couple of consumers and you manage the coupling well—then creating a bunch of DTOs “just in case” might be premature optimization.

Using CQRS to Illustrate DTOs in Action

Command Query Responsibility Segregation (CQRS) is a great example to illustrate the utility of DTOs. In CQRS, you separate your system into two parts:

  • Queries: Responsible for data retrieval and composition, returning data tailored for specific use cases.
  • Commands: Responsible for intent and actions, like updating or deleting data.

In this pattern, your query responses are essentially DTOs that contain all the composed data needed for a particular use case. For example, if you’re building a UI to display shipment details, your query DTO might include not only the shipment data but also customer details and available actions like cancel or arrive.

Commands, on the other hand, represent specific actions you want to perform. Instead of thinking in terms of generic CRUD operations, you think in terms of domain-specific commands like “CancelShipment” or “MarkShipmentArrived.” These commands encapsulate intent and behavior rather than just raw data updates.

It’s important to note that CQRS doesn’t mean you need different databases or schemas for commands and queries. It’s more about how you model your system and its interactions. Your commands and queries might operate on the same underlying database but present different models and DTOs tailored for their specific purposes.

Thinking About Data Inside and Outside Your System

One of the most important ways to think about DTOs is in terms of inside and outside data:

  • Inside: Data that is internal and private to your system. This is your implementation detail, such as your database schema or domain models.
  • Outside: Data that is exposed publicly, such as API contracts or messages sent over a queue.

The goal is to keep your internal data private so you can evolve and change it without impacting external consumers. At the same time, the data you expose externally should be treated as a contract. This contract needs to be versioned and managed carefully because you don’t control all the consumers.

So, do you always need to create DTOs? The answer depends on the context. If you own both sides of the interaction and can manage coupling effectively, you might not need DTOs everywhere. But if you expose data to external parties or want to decouple layers to allow independent evolution, DTOs are invaluable.

How Many Consumers Do You Have? Managing Coupling in Practice

Another way I think about this is by asking: how many consumers do you have that depend on your internal data model?

  • If it’s only two consumers and you’re managing coupling well, you might be fine returning your entity directly without DTOs.
  • If it’s 50 or more consumers, it’s probably a sign that you need to introduce a layer of abstraction with DTOs to avoid breaking changes and to protect your internal models.

Don’t create DTOs just to create them or because of dogma. Create them when they solve real problems like managing coupling or enabling composition.

Don’t Let DTOs Become Excessive

One of the horror stories I often hear is about layers upon layers of mappings—DTOs mapped to other DTOs, which are then mapped to yet more DTOs. This excessive mapping often adds complexity without real benefit. If your DTOs and entities are almost identical and you’re just copying data back and forth, you’re probably doing too much.

Use DTOs wisely. They should serve a clear purpose, such as:

  • Decoupling internal data models from external contracts
  • Composing data from multiple sources for specific use cases
  • Enabling versioning and evolution of your public API

When they don’t serve these purposes, reconsider whether you need them.

Summary: When and Why to Use DTOs

Let me wrap up with a few key takeaways about DTOs:

  1. DTOs are primarily about managing coupling and data composition. They help you avoid exposing your internal implementation details and allow you to shape data tailored for specific consumers.
  2. Don’t create DTOs just because of dogma or premature optimization. If your internal models and external consumers are tightly controlled and limited, you might be fine returning entities directly.
  3. Beware of excessive mapping layers. Multiple layers of DTOs that mirror each other one-to-one add unnecessary complexity.
  4. Use patterns like CQRS to think differently about queries and commands. This helps you model your data and actions in a way that naturally fits DTOs and composition.
  5. Think about data as internal (private) and external (public contract). This mindset helps you decide where DTOs are necessary and how to version your API.

Join CodeOpinon!
Developer-level members of my Patreon or YouTube channel get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.