# Why Separating DTOs from Domain Models Matters in Swift App Architecture

Let me be clear:  
I *hate* having to write both DTOs and domain models. It feels like double work. It clutters the codebase. It adds yet another layer to think about when you're just trying to get an API to show some data on screen.

And yet... I do it because when I didn’t, it blew up in my face later.

This post breaks down what DTOs and domain models are, why separating them is important, and provides a few real-world examples of how skipping that step can backfire.

---

## Let’s Talk About the Basics

### DTO (Data Transfer Object)

A DTO is a **data container** used to exchange information across boundaries — usually between a remote API and your app.

Think: Backend response → DTO → App

It represents exactly what the backend gives you. No assumptions, no transformations, just raw data.

```swift
struct UserDTO: Codable {
    let id: Int
    let first_name: String
    let last_name: String
    let email_address: String
    let created_at: String
}
```

### Domain Model

The domain model reflects the structure and language of **your app’s business logic**. It’s what the UI, business rules, and features should operate on.

```swift
struct User {
    let id: Int
    let fullName: String
    let email: String
    let joinDate: Date
}
```

Notice how this version:

* Combines `first_name` + `last_name`
    
* Converts `created_at` (a string) into a `Date`
    
* Renames fields for clarity and Swift naming conventions
    

## Why the Separation Matters

### **Reason** #1: Domain Logic Shouldn’t Care About Backend Garbage

Sometimes APIs give you fields that don’t belong in your app logic at all:

* Debug-only fields
    
* Internal backend IDs
    
* Raw numeric codes instead of enums
    
* **Deeply nested structures you don’t actually need**
    

Let’s say you receive a JSON like this:

```json
{
  "meta": {
    "request_id": "abc123",
    "timestamp": "2024-01-01T00:00:00Z"
  },
  "data": {
    "user": {
      "id": 42,
      "profile": {
        "first_name": "John",
        "last_name": "Appleseed",
        "email": "john@example.com"
      },
      "roles": [
        {
          "id": 1,
          "label": "admin",
          "permissions": ["READ", "WRITE", "DELETE"]
        }
      ],
      "audit": {
        "created_by": "system",
        "created_at": "2023-05-01T10:00:00Z",
        "updated_at": "2023-06-01T15:30:00Z"
      }
    }
  }
}
```

#### What *should* you do?

First, define a clean domain model:

```swift
struct User {
    let id: Int
    let fullName: String
    let email: String
    let role: String
}
```

Then, write a DTO and map what you need:

```swift
struct UserResponseDTO: Codable {
    let data: DataNode
    struct DataNode: Codable {
        let user: UserNode
    }
    ...
}

extension UserResponseDTO {
    func toDomain() -> User {
        let user = data.user
        return User(
            id: user.id,
            fullName: "\(user.profile.first_name) \(user.profile.last_name)",
            email: user.profile.email,
            role: user.roles.first?.label ?? "unknown"
        )
    }
}
```

Now your domain model is clean, shallow, and safe — and your app no longer lives at the mercy of backend nesting changes.

### **Reason** #2: Backend Contracts Change

Ever had a backend team rename a field because they “standardized the naming convention”? One day it’s `email_address`. Next day? `email`. No warning. No version bump. Just… surprise!

If you were piping that directly into your SwiftUI view, you’ve just earned yourself a lovely `nil` crash and a bunch of angry users.

But if that chaos hit a **DTO**, you’d be fine (only one file change).

```swift
enum CodingKeys: String, CodingKey {
    case emailAddress = "email"
}
```

The UI wouldn’t even notice the explosion behind the scenes. That’s the point. DTOs act like airbags — ugly, but essential when things go wrong.

### **Reason** #3: Different Sources, One Model

Suppose your app gets `User` data from:

* An API
    
* A local cache
    
* A CoreData store
    

All of them return different formats, and some even lack fields.

**Solution**: Use DTOs per source and map them into a consistent domain model.

```swift
extension UserDTOFromAPI {
    func toDomain() -> User {
        User(
            id: id, 
            fullName: "\(first_name) \(last_name)", 
            email: email_address,
            joinDate: created_at.toDate()
        )
    }
}

extension UserDTOFromLocalCache {
    func toDomain() -> User {
        User(
            id: userId, 
            fullName: name, 
            email: email, 
            joinDate: Date(timeIntervalSince1970: timestamp)
        )
    }
}
```

## Final Thoughts

Yes, writing both DTOs and domain models feels like *extra work* most of the time, but the truth is: if you skipped them, you’ll pay for it later — in broken UIs, fragile tests, and spaghetti logic tied directly to whatever the backend felt like returning that week. So now I treat them like seatbelts. You don’t put them on because you’re expecting a crash; you put them on because you *know* what happens when you don’t.

If you want your Swift app to scale, survive backend changes, and stay testable and sane, separate your DTOs and your domain logic.  
Future you will thank you.
