Domain Primitives, Swift and You

Robert Clark
7 min readDec 16, 2019

--

Domain Primitives are a powerful concept which can help productivity, simplification and safety in a codebase. If you aren’t familiar with domain primitives, this article will give you an overview of what they are and their benefits.

Here’s a concise definition of Domain Primitives from the free online chapter of the Manning book Secure By Design:

A value object precise enough in its definition that it, by its mere existence, manifests its validity is called a domain primitive.

To understand value objects and domain primitives I will work through a simple example. Let’s say our app needs to display books and their prices, and prices may be in several currencies. We’ll start with a simple Book entity:

A Book entity

Book is an entity because it is not defined by its properties, but rather its identity — that which doesn’t change over time.

When it comes to any operation concerning a books price, we need to consider both priceAmount and priceCurrency. For example when rendering a books price to the user, or when computing discounts.

Next we’ll improve this by evolving our model. The price information moves into a value objectthese are immutable types which contain attributes, but they have no conceptual identity.

Our value object manifests as a Money struct:

A Book entity with a Money value object

Although Money is a fairly basic value object, its presence tells us that amount and currency belong together as part of a conceptual whole. Other code becomes simpler as it now refers to the Money type. For example to calculate the total value of an order (say several books plus shipping), we only need to compute across Money instances. Beforehand, our code would have pulled together prices and currencies from different parts of our domain, each with potentially different representations and constraints.

For our Money type to qualify as a domain primitive though, we need to only allow valid instances. We’ll achieve this by extracting Currency into an enum (another domain primitive!) and by introducing a failable initializer on Money:

A Book entity with Money and Currency domain primitives

Here’s how we’ve represented Currency:

Our Currency domain primitive

Although our Currency uses standardised currency codes from ISO-4217, only four are used here. It’s typical to redefine validity like this — your domain’s notion of validity can be quite different than other domains, including standards!

Also, since our Currency enum extends String, we already have the failable initializerinit?(rawValue: String) which allows safe instantiation from raw strings. Having the validation built into a constructor like this keeps code simpler. Previously the codebase probably would have had additional code performing the equivalent validation.

Next lets look at our Money struct:

The interesting part here is how we’ve allowed only valid Money instances.

1 — The first initializer takes a currency and decimal amount. Currency must already be valid, and the amount is rounded to ensure we don’t hold additional precision which might cause problems later.

2 — The second initializer is failable and attempts to instantiate Money from currency code and formatted amount strings. It also takes a Locale parameter used when parsing the decimal string representation. Most likely our string value comes from an API using a known Locale, and the one we use locally needs to match:

// ✅ Using our '.appDefault' locale (which is "en_US")
Money(currencyCode: "USD",
amountString: "12,345.56")
// ✅ Using a German locale which expects different decimal
// point and thousands separators.
Money(currencyCode: "USD",
amountString: "12.345,56",
locale: Locale(identifier: "de_DE"))
// ❌ Mismatched locale and amountString return nil, as expected
Money(currencyCode: "USD",
amountString: "12.345,56")

Domain Primitive Checklist

Let’s walk though a checklist of domain primitive qualities and relate how we’ve addressed them here.

1. Domain primitives are building blocks which are conceptual wholes

Our Currency type is a conceptual whole because it encapsulates valid international currencies within our domain. Our Money domain primitive is a conceptual whole representing a monetary amount in one of our supported currencies.

2. Domain primitives are immutable

Our domain primitives are swift enums and structs, which are by nature immutable. We avoid using mutating functions.

3. Domain primitives are valid when existent

By definition, swift enums must be valid. For our structs however we use failable initializers which contain validation rules which must pass for initialization to succeed. Instances of both Currency and Money can only be valid.

(Note — an alternative to failable initializers is to make the initializer private, then expose static factory functions on the type. This achieves a similar outcome, but my personal preference is to use initializers)

4. Domain primitives are neither generic…

Our domain primitives are not generic — instead they make their full type explicit in their signature.

5. …nor language primitives

Domain primitives cannot be language primitives, because language primitives aren’t constrained and may bring behaviour we don’t want. We can see these problems when typealiasing domain primitive names to language primitives:

typealias Currency = String
let NZD = Currency("NZD")
typealias ISBN = String
let myIsbn = ISBN("978-3-16-148410-0")
let meaninglessConcatenation = NZD + myIsbn // ???

Not only is there no validation occurring, but we’re able to concatenate together a Currency and ISBN, which is bizarre if you think about it! The situation is similar with numeric primitives — for example, math operations which can be used across types, which usually makes no sense.

With typealiasing you don’t get compiler type checking either — which means that a function evaluate(isbn: ISBN) can happily have the typealiased Currency passed into its isbn parameter.

The point here is that with domain primitives the type checker prevents these mistakes from being possible. If you wish to have operations on your domain primitives, you get to define them yourself.

Checklist item 6. Domain primitives are types whose values define their identity

Domain primitives don’t have separate identifiers (like entities do). Instead the conceptual “identity” of a domain primitive is its values. In swift we can easily make our domain primitives adopt Equatable to allow them to be compared, which can be useful for value comparisons:

// Currency is already Equatable by defaultCurrency.NZD == Currency.USD // ❌ false
Currency.NZD == Currency.NZD // ✅ true
// We need to make Money Equatable explicitly
extension Money : Equatable {}
let money1 = Money(currencyCode: "USD",
amountString: "32,113.01")
let money2 = Money(currencyCode: "USD",
amountString: "32.113,013",
locale: Locale(identifier: "de_DE"))
money1 == money2 // ✅ true

How do I decide what qualifies as a domain primitive?

What may qualify as a domain primitive differs across domains, so there is no hard-and-fast rule about what is and isn’t a domain primitive. Having said that, here are some general guidelines for discovering candidates.

Guideline 1 — Unconstrained language primitives

You’ve most likely seen unconstrained language primitives holding values which in reality have limits. We saw this earlier when we typealiased Currency and ISBN to strings. Similarly, a numeric postcode can be an integer, but any integer isn’t a postcode.

Having unconstrained primitives is a strong sign of a potential domain primitive. Typical candidates are simple types such as PhoneNumber, Email, Postcode, ISBN or Currency.

Guideline 2 — Conflicting assumptions around types

Sometimes a relatively simple Foundation object like a Date can be used in many ways. It may be a timezone-agnostic calendar date (such as a birthday) or a timezone-aware point-in-time. These different assumptions and usages signal that the humble date object could be more safely used if lifted into a more meaningful domain primitive.

Guideline 3 — Scattered validation

We’ve seen how the validation in domain primitives is encapsulated within initializers. Without this discipline, validation rules tend to be defined further away from the objects they apply to. As team members come and go this can lead to difficulty discovering validation, and we may forget it or end up rewriting “equivalent” validation more than once —unfortunately I have seen this several times!

Guideline 4 — Repeated structures

Repeated structures representing conceptual wholes are clues that domain primitives can be factored out. Aside from refactoring being a good practice, by purposefully applying the domain primitive guidelines here we gain all the other benefits. Candidates could be objects such as Addresses, Durations, or Schedules.

Summary

We explored what value objects and domain primitives are by starting with a simple Book entity and evolved it to use Currency and Money domain primitives.

We went through a checklist of domain primitive qualities and saw how our example complied.

Finally, I summarised some general guidelines for how to discover domain primitive candidates in your codebase.

I encourage you to explore the ideas in this article, and I also recommend these resources which have been an inspiring source for me:

--

--

Robert Clark

iOS architecture & team specialist. Living with family in Wellington, NZ