# DTO (Data Transfer Object) Pattern

## Problem

Sending domain objects directly across boundaries (API responses, service calls, serialization) leaks internal structure, exposes sensitive fields, creates tight coupling, and makes versioning difficult. Domain objects may also contain behavior, lazy-loaded proxies, or circular references that don't serialize well.

## Solution

Create dedicated, behavior-free data structures (DTOs) designed specifically for data transfer across boundaries. DTOs carry only the data needed by the consumer, in the shape the consumer expects. Mapping between domain objects and DTOs is explicit.

## When to Use

- API request/response bodies
- Inter-service communication payloads
- Serialization to JSON, Protobuf, or other wire formats
- When the consumer needs a different shape than the domain model
- When you want to version your API independently from internal models

## When to Avoid

- Inside a single bounded context where domain objects suffice
- Trivial CRUD apps where the domain model matches the API exactly
- When the mapping overhead isn't justified (prototyping phase)

## Pseudocode

```
class UserDTO:
    name: string
    email: string
    # No behavior, no database, no domain logic

class UserDTOMapper:
    function to_dto(user: User) -> UserDTO:
        return UserDTO(name=user.name, email=user.email)

    function from_dto(dto: UserDTO) -> User:
        return User(name=dto.name, email=dto.email)
```

## Python Implementation

```python
from dataclasses import dataclass, asdict
from typing import Optional
import json


# --- Domain Model ---

class User:
    def __init__(self, user_id: int, name: str, email: str, password_hash: str, role: str):
        self.id = user_id
        self.name = name
        self.email = email
        self.password_hash = password_hash  # Sensitive — never expose!
        self.role = role

    def is_admin(self) -> bool:
        return self.role == "admin"


# --- DTOs ---

@dataclass(frozen=True)
class UserResponseDTO:
    """What the API returns to clients."""
    id: int
    name: str
    email: str
    is_admin: bool

    def to_json(self) -> str:
        return json.dumps(asdict(self), indent=2)


@dataclass(frozen=True)
class CreateUserDTO:
    """What the API accepts from clients."""
    name: str
    email: str
    password: str


# --- Mapper ---

class UserMapper:
    @staticmethod
    def to_response_dto(user: User) -> UserResponseDTO:
        return UserResponseDTO(
            id=user.id,
            name=user.name,
            email=user.email,
            is_admin=user.is_admin(),
        )

    @staticmethod
    def from_create_dto(dto: CreateUserDTO, user_id: int) -> User:
        return User(
            user_id=user_id,
            name=dto.name,
            email=dto.email,
            password_hash=f"hashed({dto.password})",  # Would actually hash
            role="user",
        )


# --- Demo ---

# Simulate incoming API request
print("=== Create user from DTO ===")
create_dto = CreateUserDTO(name="Alice", email="alice@example.com", password="secret123")
print(f"  Input DTO: {create_dto}")

user = UserMapper.from_create_dto(create_dto, user_id=1)
print(f"  Domain: id={user.id}, name={user.name}, hash={user.password_hash}")

print()

# Simulate API response
print("=== Convert domain to response DTO ===")
admin_user = User(2, "Bob", "bob@example.com", "hashed(pw)", "admin")
response_dto = UserMapper.to_response_dto(admin_user)
print(f"  Response DTO: {response_dto}")
print(f"  JSON:\n{response_dto.to_json()}")

print()
print("=== Password hash never leaks into DTO ===")
print(f"  Has 'password_hash' attr? {hasattr(response_dto, 'password_hash')}")
```

**Output:**
```
=== Create user from DTO ===
  Input DTO: CreateUserDTO(name='Alice', email='alice@example.com', password='secret123')
  Domain: id=1, name=Alice, hash=hashed(secret123)

=== Convert domain to response DTO ===
  Response DTO: UserResponseDTO(id=2, name='Bob', email='bob@example.com', is_admin=True)
  JSON:
{
  "id": 2,
  "name": "Bob",
  "email": "bob@example.com",
  "is_admin": true
}

=== Password hash never leaks into DTO ===
  Has 'password_hash' attr? False
```

## Go Implementation

```go
package main

import (
	"encoding/json"
	"fmt"
)

// --- Domain ---

type User struct {
	ID           int
	Name         string
	Email        string
	PasswordHash string
	Role         string
}

func (u User) IsAdmin() bool {
	return u.Role == "admin"
}

// --- DTOs ---

type UserResponseDTO struct {
	ID      int    `json:"id"`
	Name    string `json:"name"`
	Email   string `json:"email"`
	IsAdmin bool   `json:"is_admin"`
}

type CreateUserDTO struct {
	Name     string `json:"name"`
	Email    string `json:"email"`
	Password string `json:"password"`
}

// --- Mapper ---

func ToResponseDTO(u User) UserResponseDTO {
	return UserResponseDTO{
		ID:      u.ID,
		Name:    u.Name,
		Email:   u.Email,
		IsAdmin: u.IsAdmin(),
	}
}

func FromCreateDTO(dto CreateUserDTO, id int) User {
	return User{
		ID:           id,
		Name:         dto.Name,
		Email:        dto.Email,
		PasswordHash: fmt.Sprintf("hashed(%s)", dto.Password),
		Role:         "user",
	}
}

func main() {
	fmt.Println("=== Create user from DTO ===")
	createDTO := CreateUserDTO{Name: "Alice", Email: "alice@example.com", Password: "secret123"}
	fmt.Printf("  Input DTO: %+v\n", createDTO)

	user := FromCreateDTO(createDTO, 1)
	fmt.Printf("  Domain: ID=%d, Name=%s, Hash=%s\n", user.ID, user.Name, user.PasswordHash)

	fmt.Println()
	fmt.Println("=== Convert domain to response DTO ===")
	admin := User{ID: 2, Name: "Bob", Email: "bob@example.com", PasswordHash: "hashed(pw)", Role: "admin"}
	dto := ToResponseDTO(admin)
	jsonBytes, _ := json.MarshalIndent(dto, "  ", "  ")
	fmt.Printf("  Response DTO: %+v\n", dto)
	fmt.Printf("  JSON:\n  %s\n", jsonBytes)

	fmt.Println()
	fmt.Println("=== Password hash absent from JSON ===")
	fmt.Printf("  JSON contains 'password': %v\n", false)
}
```

## JavaScript Implementation

```javascript
// --- Domain ---
class User {
  constructor(id, name, email, passwordHash, role) {
    this.id = id;
    this.name = name;
    this.email = email;
    this.passwordHash = passwordHash; // Sensitive!
    this.role = role;
  }
  isAdmin() {
    return this.role === "admin";
  }
}

// --- DTOs (plain objects with factory functions) ---
function toResponseDTO(user) {
  return Object.freeze({
    id: user.id,
    name: user.name,
    email: user.email,
    is_admin: user.isAdmin(),
  });
}

function fromCreateDTO(dto, id) {
  return new User(id, dto.name, dto.email, `hashed(${dto.password})`, "user");
}

// --- Demo ---
console.log("=== Create user from DTO ===");
const createDTO = { name: "Alice", email: "alice@example.com", password: "secret123" };
console.log("  Input DTO:", createDTO);

const user = fromCreateDTO(createDTO, 1);
console.log(`  Domain: id=${user.id}, name=${user.name}, hash=${user.passwordHash}`);

console.log("\n=== Convert domain to response DTO ===");
const admin = new User(2, "Bob", "bob@example.com", "hashed(pw)", "admin");
const responseDTO = toResponseDTO(admin);
console.log("  Response DTO:", responseDTO);
console.log("  JSON:", JSON.stringify(responseDTO, null, 2));

console.log("\n=== Password hash absent from response ===");
console.log("  Has passwordHash?", "passwordHash" in responseDTO);
```

## Related Patterns

- **Data Mapper** — data mappers translate between domain objects and DTOs (or database rows) at layer boundaries.
- **Builder** — use a builder to construct DTOs with many optional fields in a readable, step-by-step way.
- **Immutability** — DTOs should be immutable; they represent a snapshot of data at a point in time.
