DTO (Data Transfer Object) dalam Arsitektur Backend
5 min read

DTO (Data Transfer Object) dalam Arsitektur Backend

DTO (Data Transfer Object) adalah salah satu konsep paling penting—dan paling sering disalahpahami—dalam pengembangan backend modern. Banyak kode menjadi sulit dirawat, rapuh, dan bocor antar layer bukan karena business logic yang kompleks, tapi karena DTO digunakan secara sembarangan.

Artikel ini akan membahas secara mendetail dan praktis:

  • Apa itu DTO dan kenapa dibutuhkan
  • Jenis-jenis DTO (Request, Query Result, Response)
  • DTO digunakan di layer mana
  • Contoh implementasi nyata (CRUD + raw query)
  • Anti-pattern yang sering terjadi

Fokus artikel ini adalah backend service (REST/gRPC), dengan contoh utama menggunakan Golang dan repository pattern.


Apa Itu DTO?

DTO (Data Transfer Object) adalah objek yang digunakan khusus untuk membawa data antar layer atau antar boundary sistem, tanpa mengandung business logic.

DTO bukan entity, bukan model database, dan bukan domain object.

Karakteristik DTO:

  • Struktur sederhana (struct / class)
  • Tidak punya behavior
  • Digunakan sebagai kontrak data

Kenapa DTO Penting?

Tanpa DTO:

  • Handler langsung pakai model database
  • Repository tahu detail HTTP
  • Perubahan schema DB memecahkan API

Dengan DTO:

  • Boundary antar layer jelas
  • API stabil walau DB berubah
  • Lebih aman dan maintainable

Layer dalam Backend (Konteks DTO)

Struktur umum:

Handler (HTTP / gRPC)
   ↓
Service (Business Logic)
   ↓
Repository (Data Access)
   ↓
Database

DTO selalu berada di boundary antar layer, bukan di tengah-tengah logic.


Jenis-Jenis DTO

Request DTO

Request DTO merepresentasikan input dari client.

Tujuan

  • Mendefinisikan apa yang BOLEH dikirim client
  • Validasi input
  • Melindungi domain dari data liar

Digunakan di Layer

  • Handler
  • Service (sebagai parameter)

Contoh: Create Request DTO

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
}

Contoh: Update Request DTO

type UpdateUserRequest struct {
    Name  *string `json:"name,omitempty"`
    Email *string `json:"email,omitempty"`
}

Pointer digunakan agar bisa membedakan:

  • nil → tidak diupdate
  • "" → update ke string kosong

List / Query Request DTO

DTO ini digunakan untuk:

  • Filtering
  • Sorting
  • Pagination

Digunakan di Layer

  • Handler
  • Service

Contoh: List Request DTO

type ListUserRequest struct {
    Page   int     `query:"page"`
    Limit  int     `query:"limit"`
    SortBy string  `query:"sort_by"`
    Order  string  `query:"order"`

    Status *string `query:"status"`
    Q      *string `query:"q"`
}

DTO ini adalah kontrak API, bukan kontrak database.

Query Result DTO (Raw Query DTO)

Ini adalah DTO yang sering terlupakan, padahal sangat penting.

Query Result DTO digunakan untuk:

  • Menampung hasil raw SQL
  • Join kompleks
  • Aggregation (COUNT, SUM, GROUP BY)

Kenapa Tidak Pakai Entity?

  • Field tidak 1:1 dengan tabel
  • Bisa gabungan banyak tabel
  • Bisa computed field

Digunakan di Layer

  • Repository (output)
  • Service (input)

Contoh: Query Result DTO

type UserStatsRow struct {
    UserID      uint   `db:"user_id"`
    Name        string `db:"name"`
    TotalOrders int    `db:"total_orders"`
}

Contoh Repository dengan Raw Query

func (r *userRepository) FindUserStats(ctx context.Context) ([]UserStatsRow, error) {
    var rows []UserStatsRow

    query := `
        SELECT u.id AS user_id, u.name, COUNT(o.id) AS total_orders
        FROM users u
        LEFT JOIN orders o ON o.user_id = u.id
        GROUP BY u.id, u.name
    `

    err := r.db.WithContext(ctx).Raw(query).Scan(&rows).Error
    return rows, err
}

⚠️ Jangan memaksakan hasil raw query masuk ke entity.

Response DTO

Response DTO merepresentasikan data yang dikirim ke client.

Tujuan

  • Menyaring field sensitif
  • Format data untuk API
  • Menjaga backward compatibility

Digunakan di Layer

  • Service
  • Handler

Contoh: Response DTO

type UserResponse struct {
    ID    uint   `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

Contoh Flow Lengkap (Create User)

Handler

var req dto.CreateUserRequest
if err := c.BodyParser(&req); err != nil {
    return err
}

user, err := userService.Create(ctx, req)

Service

func (s *userService) Create(ctx context.Context, req dto.CreateUserRequest) (*dto.UserResponse, error) {
    user := model.User{
        Name:  req.Name,
        Email: req.Email,
    }

    if err := s.repo.Create(ctx, &user); err != nil {
        return nil, err
    }

    return &dto.UserResponse{
        ID:    user.ID,
        Name:  user.Name,
        Email: user.Email,
    }, nil
}

DTO vs Entity (Ringkasan Penting)

AspekDTOEntity
TujuanTransfer dataRepresentasi DB
Logic❌ Tidak ada❌ (idealnya)
StabilRelatif stabilBisa berubah
DigunakanBoundaryRepository

Anti-Pattern yang Harus Dihindari

❌ Repository menerima Request DTO

❌ Handler mengembalikan Entity langsung

❌ Satu DTO untuk Create, Update, dan List

❌ Raw query langsung scan ke entity


DTO Adapter (Mapper) untuk Scalability

Seiring bertambahnya kompleksitas sistem, proses mapping antara DTO ↔ Entity ↔ Query Result akan semakin sering dan semakin kompleks. Di sinilah Adapter (atau Mapper) berperan penting.

Apa Itu DTO Adapter?

DTO Adapter adalah komponen khusus yang bertugas:

  • Mengubah Request DTO → Entity / Command
  • Mengubah Entity / Query Result → Response DTO

Adapter tidak berisi business logic, hanya transformasi data.

DTO ↔ Adapter ↔ Domain / Entity

Kenapa Adapter Dibutuhkan untuk Scalability?

Tanpa adapter:

  • Mapping tersebar di service
  • Service menjadi gemuk
  • Sulit refactor atau reuse

Dengan adapter:

  • Service fokus ke business logic
  • Mapping konsisten & terpusat
  • Mudah menambah variasi response
Tanpa AdapterDengan Adapter
Service gemukService tipis
Mapping duplikatifMapping terpusat
Sulit reuseMudah reuse

Kapan Adapter Mulai Wajib Digunakan?

Adapter sangat direkomendasikan jika:

  • DTO dan Entity mulai tidak 1:1
  • Banyak variasi response
  • Raw query makin kompleks
  • Domain logic berkembang

Pada project kecil, mapping langsung di service masih wajar.

Contoh Adapter: Request DTO → Entity

type UserAdapter struct{}

func (a UserAdapter) ToEntity(req dto.CreateUserRequest) model.User {
    return model.User{
        Name:  req.Name,
        Email: req.Email,
    }
}

Contoh Adapter: Entity → Response DTO

func (a UserAdapter) ToResponse(user model.User) dto.UserResponse {
    return dto.UserResponse{
        ID:    user.ID,
        Name:  user.Name,
        Email: user.Email,
    }
}

Contoh Adapter: Query Result DTO → Response DTO

func (a UserAdapter) StatsToResponse(row UserStatsRow) dto.UserStatsResponse {
    return dto.UserStatsResponse{
        UserID:      row.UserID,
        Name:        row.Name,
        TotalOrders: row.TotalOrders,
    }
}

Pemakaian Adapter di Service

func (s *userService) Create(ctx context.Context, req dto.CreateUserRequest) (*dto.UserResponse, error) {
    user := s.adapter.ToEntity(req)

    if err := s.repo.Create(ctx, &user); err != nil {
        return nil, err
    }

    resp := s.adapter.ToResponse(user)
    return &resp, nil
}

Service tetap bersih, fokus, dan scalable.

Adapter vs Mapper vs Assembler

Secara konsep:

  • Adapter → istilah umum (clean architecture)
  • Mapper → istilah populer di backend
  • Assembler → istilah klasik DDD

Secara fungsi, tujuannya sama.

Anti-Pattern Adapter ❌

  • Adapter mengandung business logic
  • Adapter memanggil repository
  • Adapter melakukan validasi

Adapter hanya untuk transformasi data.

Posisi Adapter dalam Layer

Handler
   ↓
DTO
   ↓
Adapter  ← HERE
   ↓
Service
   ↓
Repository

Penutup

  • DTO adalah kontrak antar layer
  • Setiap jenis operasi butuh DTO yang berbeda
  • Request, Query Result, dan Response DTO punya tanggung jawab masing-masing
  • Repository tidak tahu HTTP
  • Service adalah tempat mapping DTO ↔ Entity

DTO yang dirancang dengan baik membuat sistem lebih aman, scalable, dan mudah dirawat.