Swift/UnwrapDiscover about Swift, iOS and architecture patterns

Data and Domain models: Learn to use them

June 27, 2023

With SwiftData coming out many people will throw themselves into making a “model”.

But did you ever think what is a model? Or what it its role or objective?

Like when talking about AI there are many kinds of models! Ok maybe not that many… today I'll talk the two I know of and use: Data (DTO) and Domain.

Never heard about them? Then there’re a high chance you did a hybrid one by mixing the two concepts 👽. Let see what makes them so different and how/when you should use them.

Data: all you data are belong to us

Let’s start with the most usual one: Data model aslo known as DTO (Data Transfer Object). It represents where data are coming from (or going to): HTTP, database, filesystem… All those technologies stores data. So whenever you use them to read/write data you effectively use a DTO.

Here a some of them:

  • 🌐 HTTP requests
  • 🗄️ CoreData (or now SwiftData)
  • 🗃️ Realm
  • 🔑 Keychain
  • 👤 UserDefaults

The last two one might be more surprising to some of you. Indeed you store and read data from keychain and UserDefaults. So you do use (or should use) Data models when doing so.

/// an HTTP response model
struct GetUserDTO: Decodable {
	let userId: Int
	let firstname: String
	let lastname: String
  let companyId: Int
  let companyName: String
  let street: String
	let zipcode: String
  let country: String
  let preferredLanguage: String
  let darkMode: Bool
}

/// user OAuth credentials DTO stored into keychain
struct OAuthKeychainDTO: Codable {
	let token: String
	let refresh: String
	let expireAt: Date
}

One important thing to note about DTO: it heavily depends on your tech architecture 🦕. Any change to your tech stack has an impact on them.

For instance the fact you decide to use a database implies you might no be able to use some Swift primitives (like enum) or that you’ll have to do relationship tables hence a another Data model and so on.

Another example is about changing your stack: migrating from CoreData to Realm, from REST to GraphQL… all those changes impact your tech architecture therefore your Data models.

Where to use Data models?

Because of such dependency you should not use DTO into your business layers (views, view models, etc…). It might sounds obvious to some of you but I recall that apple shows examples and provides helpers to easily use CoreData/SwiftData objects from… SwiftUI views! It’s probably a easy and great to learn Swift and SwiftUI and to build prototypes or small apps but definitely not a good approach for long term apps 😉

As such DTO should only their own dedicated stack which I call Data source. In other words: no one but your CoreDataSource should use a CoreData model. One good way to « enforce » this rule is by building each DataSource as a package 📦 and keep DTO internal.

You might want to put a tool like Danger to force these rules. Otherwise one developer will surely put a DTO as public one day or another.

But if DTO are internals how do you send back “data” to upper layers? Domain model to the rescue!

Domain model

Domain is not DTO

Domain models are an abstraction on top of DTO. Be careful though: it’s not because you « abstracted » your DTO that you effectively built a Domain model! I see you SwiftData!

A Domain depicts data meaning: what should be its role and usage in your application. For instance: what does it mean to be a User? What does it mean to have Settings? When can I change them?

Compared to DTO Domain models have no dependencies (apart from core library like Foundation). And because they are built on top of nothing they are tech agnostic.

In other words: changing your tech implementation must have zero impact on it. So if you use SwiftData… you’re actually still building Data models. And that’s OK but don’t confuse it with a Domain model.

Domain is simple

Many things can change from your DTO to your Domain model implementation:

  1. 📛 Naming. In your Domain you’re allowed to « fix » naming (for example wrong spelling from the backend)
  2. 🧲 Composition/aggregation. You can split your objects into multiple smaller Domain objects. Doing so can greatly improve your code maintenance and avoid nesting relationships.
  3. 🦍 Strongly type attributes. Have a finite list of value? Use a enum. A date? Use Date, etc… Strongly typing reduce the numbers of invalid case you could have in your app making it less buggy.
  4. ❓Avoid optional properties when not needed. Associated values can be handy

What is a good Domain?

Making good Domain object is not that easy. Based on the rules edicted before here is some examples of good and abad modelling:

/// BAD
/// It's just a 1-1 with DTO with all attributes
struct User {
	let id: Int
	let firstname: String
	let lastname: String
  let companyId: Int
  let companyName: String
  let street: String
	let zipcode: String
  let country: String
  let preferredLanguage: String
  let darkMode: Bool
}

/// BAD
/// - attributes are nested
/// - Non useful parameters NOT DEFINING the user identity (useDarkMode, address)
/// - Some parameters can be unclear purposes (useDarkMode? What's this?)
struct User: Identifiable {
	let id: UUID
	let firstname: String
	let lastname: String
	let email: String
	let company: Company
	let useDarkMode: Bool
	let address: Address
}

struct Company: Identifiable {
	let id: UUID
	let name: String
}
/// GOOD
/// - Very few properties (4)
/// - No nested entities
/// - Everything define the user (it answers the question "what do I need to create a user?")
struct User: Identifiable {
	let id: Int
	let firstname: String
	let lastname: String

}

struct Address {
  let street: String
	let zipcode: String
  let country: String
}

struct Settings {
  let preferredLanguage: Locale
	let useDarkMode: Bool
}

struct Company {
	let id: Int
	let name: String
}

// Aggregate
struct UserAddress {
	let user: User
	let address: Address
}

// Aggregate
struct UserSettings {
	let user: User
	let address: Address
}

See how we did smaller objects compared to our DTO? This gives us great flexibility in our code about which data we want to give while also making future refactoring simpler and safer.

We also typed locale to use Locale so that we know we have a valid value when using Settings.

Where to use DTO and Domain objects?

While it’s a bad idea to rely on Data model everywhere in your app everyone should use Domain model! Yes I’m talking about your Data layer, your business logic but also your view code!

Let’s see for each one:

  1. Data layer: your DTO being internal you can’t use it as a return value. So best thing to do is to return a Domain object.
  2. Business layer: because all your data layers now expose Domain objects it’s super easy for your business layer to interoperate between all of them and put business rules on it
  3. View layer: I already wrote a little bit about this subject when talking about ViewData in ViewModels and Separation of concern. Your view should definitely use Domain objects! There’s little interest in not doing so apart from… adding complexity.
/// A **User** view so it uses a User object.
/// Would makes little sense to hide user object to a view called... User!
/// Also if you did small Domain object there is nothing heavy in doing so.
struct UserView: View {
	let user: User
}

Time to wrap up

I hope you learned a few things about Data (DTO) and Domain objects. And I hope you’ll embrace starting doing Domain models in your app.

Be assured: it’s a long journey and it takes time to do it properly. But it’s a very useful tool that can greatly improve your code readability and maintenance!