Stop Using DTOs in Go, It’s Not Java

dsysd dev
5 min readMay 13, 2023

--

In Java, it’s common to use Data Transfer Objects (DTOs) to transfer data between layers of an application. However, in Go, DTOs aren’t necessary and can even be harmful to the design of your application.

Photo by Michiel Leunens on Unsplash

What are DTOs?

DTOs are Java classes that contain data and have no behavior.

They’re used to transfer data between layers of an application, such as between a controller and a service layer.

DTOs are often used to encapsulate data and provide a layer of abstraction between the layers of an application.

Why use DTOs in Java?

Java is a strongly typed language, which means that variables and methods must be declared with a specific data type.

This can make it difficult to pass data between layers of an application, as each layer may require a different data type.

DTOs provide a way to encapsulate data and provide a layer of abstraction between the layers of an application.

Why not use DTOs in Go?

Go is a statically typed language, like Java, but it has some key differences. One of the most important differences is that Go is a more concise language than Java.

This means that Go programs tend to have fewer layers than Java programs.

In Go, it’s common to use structs to represent data.

Structs can contain data and behavior, and can be passed between layers of an application.

This eliminates the need for DTOs, as structs can provide the same abstraction and encapsulation of data as DTOs.

The problem with DTOs in Go

Using DTOs in Go can actually be harmful to the design of your application.

DTOs add an unnecessary layer of abstraction between layers of an application, which can make your code harder to read and maintain.

DTOs can also make it harder to add new features to your application, as changes to DTOs can cause a ripple effect throughout your codebase.

Structs in Go

In Go, structs can be used to represent data and can be passed between layers of an application.

Structs can also contain behavior, making them more powerful than DTOs. Structs can be defined like this:

type Person struct {
Name string
Age int
}

Structs can be passed between functions and layers of an application, just like DTOs.

For example, here’s a function that takes a Person struct as an argument:

func PrintPerson(p Person) {
fmt.Printf("Name: %s\nAge: %d\n", p.Name, p.Age)
}

This function can be called with a Person struct, like this:

p := Person{Name: "Alice", Age: 25}
PrintPerson(p)

An Example

Let’s say we have an e-commerce application with the following layers:

  1. HTTP Layer: Responsible for handling incoming HTTP requests and returning responses.
  2. Service Layer: Responsible for handling business logic and communicating with the data layer.
  3. Data Layer: Responsible for handling communication with the database.

Now, let’s say we want to add a new feature to our application that allows users to upload images for their products. To do this, we need to modify our HTTP layer to accept image uploads, modify our service layer to handle image uploads, and modify our data layer to store the images in the database.

If we’re using DTOs to transfer data between layers, we would need to modify our DTOs to include the image data, which would then cause a ripple effect throughout our codebase. For example:

  1. We would need to modify our HTTP layer to accept the image data as part of the HTTP request and map it to our DTO.
  2. We would need to modify our service layer to update our DTOs to include the image data and handle the image upload logic.
  3. We would need to modify our data layer to update our DTOs to include the image data and store the images in the database.

As you can see, this can quickly become tedious and error-prone. Additionally, if we’re not careful, we could end up with DTOs that contain a lot of unnecessary data, making them harder to work with and slowing down our application.

A better approach would be to use structs directly in our application layers, without the need for DTOs. This approach would simplify our code and make it easier to add new features to our application without causing a ripple effect throughout our codebase.

The Subtle and Minute Differences between the usages

// User struct used directly in application layer
type User struct {
ID int
FirstName string
LastName string
Email string
}

func GetUser(userID int) (*User, error) {
// Get user from the database
dbUser, err := db.GetUserByID(userID)
if err != nil {
return nil, err
}

// Map database user to User struct
user := &User{
ID: dbUser.ID,
FirstName: dbUser.FirstName,
LastName: dbUser.LastName,
Email: dbUser.Email,
}

return user, nil
}

// UserDTO struct used in data transfer between layers
type UserDTO struct {
ID int
FirstName string
LastName string
Email string
}

func UpdateUser(userDTO *UserDTO) error {
// Map UserDTO to User struct
user := &User{
ID: userDTO.ID,
FirstName: userDTO.FirstName,
LastName: userDTO.LastName,
Email: userDTO.Email,
}

// Update user in the database
err := db.UpdateUser(user)
if err != nil {
return err
}

return nil
}

The main difference is that in the second example, we’re using a User struct directly in our application layer instead of a separate UserDTO struct. This eliminates the need for an extra layer of abstraction and simplifies our code.

We’re using a User struct directly in our GetUser function, but we're still using a separate UserDTO struct for data transfer between layers. This demonstrates how we might use structs directly in our application layer, but still need to use DTOs in other parts of our codebase.

The philosohpy of Go is to keep it simple, this article might not make sense to some users but the idea is to use the same struct with embeddings and json/db annotations for different purposes as much as possible

Unlike other practices where we keep the DAO and DTO layer separate…Keep it simple as much as you can, simply use GO STRUCTs

Conclusion

DTOs are commonly used in Java to transfer data between layers of an application, but they’re not necessary in Go.

Go’s concise nature and powerful struct type make it possible to pass data between layers of an application without adding an unnecessary layer of abstraction.

If you need, you can use the Option pattern or Builder pattern to enrich your structs.

By using structs instead of DTOs, you can make your code easier to read, maintain, and extend. So, stop using DTOs in Go — it’s not Java!

Subscribe to my Youtube channel

Subscribe to my youtube channel if you are on the lookout for more such awesome content in video format.

https://www.youtube.com/@dsysd-dev

Claps Please !!

If you found this article helpful I would appreciate some claps 👏👏👏👏, it motivates me to write more such useful articles in the future.

Follow me on medium for regular awesome content and insights.

Subscribe to my Newsletter

If you like my content, then consider subscribing to my free newsletter, to get exclusive, educational, technical, interesting and career related content directly delivered to your inbox

https://dsysd.beehiiv.com/subscribe

Important Links

Thanks for reading the post, be sure to follow the links below for even more awesome content in the future.

Twitter: https://twitter.com/dsysd_dev
Youtube: https://www.youtube.com/@dsysd-dev
Github: https://github.com/dsysd-dev
Medium: https://medium.com/@dsysd-dev
Email: dsysd.mail@gmail.com
Linkedin: https://www.linkedin.com/in/dsysd-dev/
Newsletter: https://dsysd.beehiiv.com/subscribe
Gumroad: https://dsysd.gumroad.com/

--

--

dsysd dev
dsysd dev

Written by dsysd dev

Helping you become an 11x developer. I write on distributed systems, system design, blockchain, and go. https://twitter.com/dsysd_dev

Responses (65)