Rethinking DTOs in Go: Using Structs and Interfaces for Clean Data Transfer

This article explains how Go's native structs and interfaces can replace traditional Data Transfer Objects, offering a more concise, performant, and maintainable way to move data across application layers while highlighting best‑practice patterns and potential pitfalls.

Ops Development & AI Practice
Ops Development & AI Practice
Ops Development & AI Practice
Rethinking DTOs in Go: Using Structs and Interfaces for Clean Data Transfer

Introduction

In many languages, Data Transfer Objects (DTOs) are used to carry data between layers, but Go’s type system and interface design allow more flexible solutions. This article explores common Go patterns that achieve DTO‑like functionality without extra boilerplate.

Go Types and Interfaces

Go emphasizes simplicity and efficiency. Its two core constructs for data handling are:

Structs : Define and encapsulate data, often used directly to pass information between layers, eliminating the need for separate DTO types.

Interfaces : Declare method sets; any type implementing those methods satisfies the interface, providing powerful abstraction and polymorphism.

Strategies that Replace DTOs

Instead of defining explicit DTOs, Go projects typically adopt one of the following approaches:

Direct struct usage : Define a struct and pass it through the layers, reducing redundant code.

Interface isolation : Create interfaces that describe required operations, then pass structs that implement those interfaces, improving modularity and testability.

Type embedding and composition : Embed one struct within another to extend functionality, useful for handling complex data structures.

Direct Struct Example

Consider an e‑commerce platform that needs to transfer order data from the service layer to the data‑access layer. A simple Order struct can hold fields such as UserID, ItemList, and PaymentDetails:

type Order struct {
    UserID        string
    ItemList      []Item
    PaymentDetails Payment
}

func processOrder(o Order) {
    // order processing logic
}

This struct can travel from the API layer to business logic and finally to the database without additional conversion, keeping the code clean and efficient.

Advantages and Considerations

Advantages :

Performance : No extra memory allocation or object wrapping.

Simplicity : Go’s minimalist design avoids over‑engineering.

Considerations :

Data‑structure changes : Modifying a struct may impact many parts of the application.

Interface misuse : Overusing interfaces can hurt readability and performance.

Interface Isolation Example

To achieve stronger decoupling, define an OrderData interface that exposes only the needed getters:

// OrderData defines the contract for order information
type OrderData interface {
    GetID() string
    GetCustomerID() string
    GetOrderDetails() []OrderDetail
}

type OrderDetail struct {
    ProductID string
    Quantity  int
    Price     float64
}

type ConcreteOrder struct {
    ID         string
    CustomerID string
    Details    []OrderDetail
}

func (co *ConcreteOrder) GetID() string { return co.ID }
func (co *ConcreteOrder) GetCustomerID() string { return co.CustomerID }
func (co *ConcreteOrder) GetOrderDetails() []OrderDetail { return co.Details }

Next, modify the OrderProcessor interface to accept OrderData instead of a concrete Order:

// OrderProcessor processes orders via the abstract interface
type OrderProcessor interface {
    ProcessOrder(order OrderData) error
}

type APIService struct{}

func (api *APIService) ProcessOrder(order OrderData) error {
    fmt.Println("Processing order ID:", order.GetID())
    return nil
}

Using this pattern, any implementation of OrderProcessor works with any struct that satisfies OrderData, enabling reuse across services or test environments without changing the processor code.

func main() {
    apiService := APIService{}
    order := ConcreteOrder{
        ID:         "12345",
        CustomerID: "abcde",
        Details: []OrderDetail{{ProductID: "xyz", Quantity: 2, Price: 29.99}},
    }
    // Pass the order via the interface
    apiService.ProcessOrder(&order)
}

Conclusion

Go’s flexible type system allows developers to handle cross‑layer data transfer efficiently without the overhead of traditional DTO patterns. By leveraging structs for simple cases and interfaces for abstraction, code remains concise, performant, and easier to maintain.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendGodtoInterfacestructData Transfer
Ops Development & AI Practice
Written by

Ops Development & AI Practice

DevSecOps engineer sharing experiences and insights on AI, Web3, and Claude code development. Aims to help solve technical challenges, improve development efficiency, and grow through community interaction. Feel free to comment and discuss.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.