Data and Domain models: Learn to use them
June 27, 2023With 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:
- 📛 Naming. In your Domain you’re allowed to « fix » naming (for example wrong spelling from the backend)
- 🧲 Composition/aggregation. You can split your objects into multiple smaller Domain objects. Doing so can greatly improve your code maintenance and avoid nesting relationships.
- 🦍 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.
- ❓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:
- 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.
- 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
- 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!