The Difference between Entity and Value Object, and How They Relate to Swift's Identifiable and Equatable Protocols

Helge Heß recently posted on Mastodon that he “still find[s] it disturbing how many #SwiftLang devs implement Equatable as something that breaks the Equatable contract, just to please some API requiring it. Feels like they usually should implement Identifiable and build on top of that instead.”

An interesting Swift piece he shared is to not slap Hashable onto a type just to reap the benefits of Set logic and uniqueness in collections. This usually starts by conforming to Hashable but then only pick one or a few properties for the hash computation. That’s basically a code smell. Consider to use an identifier that’s hashable instead if you want to “hash by ID” anyway:

struct Foo: Identifiable {
  let id: UUID
  let otherPropertyYouWantToIgnore: String = ...
  // ...
}

// Set<Foo> then becomes:
var foos: [Foo.ID : Foo] = [...]

Never thought of that, so kudos!

Update 2023-01-18: Here’s a Dictionary.init extension to simplify getting these instead of Sets.

But I really want to bring attention to the abuse of Equatable he mentioned.

To me, it looks like users of the Swift language may be conflating the following:

  • Value Object and value type (e.g. struct and enum), and
  • Entity and reference type (e.g. class and actor).

The capitalized ones are concepts – the rest is a language construct. (I encountered the most succinct and sensible definitions of these concepts in Domain-Driven Design, but they are older.)

Entities have some kind of identity over time. In practice, this is often accompanyied by some kind of persistence. You fetch the same SQL database table row 10 times, but it’s supposed to be the same thing, no matter if the type of the resulting object in main memory has a reference type or value type. This applies to e.g. customers and products, or posts and tweets.

Value Objects, on the other hand, are considered to be equal in virtue of their data. If you have two Value Objects in your app’s memory with the same attributes (in Swift parlance: properties), then they are the same thing. This is used for stuff like money/currency, time, addresses, and names.

In Swift, i.e. in code and not in land of concepts, you can get “identity” of two objects by using the Identifiable protocol, no matter if it’s used on a struct or class.

You make different objects of the same type equal via the Equatable protocol, also no matter if used on a struct or class.

We had Value Objects in Objective-C, a language without the value types we know from Swift, by overriding isEqual: to make the NSObject subclasses compare their objects not by memory address, but by their properties. In other words, we’d ignore if two references were identical objects in memory or not.

We also had Entities in Objective-C. You could get by with memory address chcks, but if you were serious, you had some database anyway and thus would have access to an Entity ID of sorts. Could be an auto-incrementing SQLite table id, for example.

Conversely, you can have structs (a value type) represent Value Objects or Entities.

struct PersonName: Equatable {
  let firstName: String
  let middleName: String?
  let lastName: String
}

struct Person: Identifiable {
  let id: Int
  let name: PersonName
}

With this definition, it’s tempting to slap Equatable onto Person as well because you want to test if personA and personB are different. The equality operator == feels like a sensible choice, so you’d start with this:

extension Person: Equatable {
  static func == (lhs: Person, rhs: Person) -> Bool {
    lhs.id == rhs.id
  }
}

This correctly omits the name from the equality test.

It (probably incorrectly) also makes two instances of Person equatable – for the convenience of comparing two objects – while it’d have sufficed to use this in call sites:

personA.id == personB.id

This does the same thing but does drive home that you’re dealing with Entities that define same-ness via their ID and ignore the attributes.

Conforming to Equatable arguable improves the ergonomics when you type comparisons:

personA.id == personB.id
// vs
personA == personB

But the second approach also leaves to guesswork what kind of domain concept you’re trying to express. And then you cannot check if the attributes of the Person instances are also equal.

That’s the delineation of “equal” vs “identical”. Equal in terms of data, identical in terms of it being an Entity.

Let’s get back to the false conflation of “Value Object” and “value type”: once you find yourself using Equatable (or Hashable) in a struct (a value type) and then also limit it to a small subset of properties to express when two instances should be considered identical, you maybe (ab-)using equatability to express identity. Try to use Identifiable instead.

If you care about modeling a rich domain, consider these rules of thumb:

  • Express identity/same-ness of an Entity via the Identifiable protocol;
  • Express same-ness of Value Objects via their data using the Equatable protocol;
  • Instead of (ab-)using Hashable to get Set semantics for cheap uniqueness on a subset of properties, use a hashable identifier instead. Replace Set<Foo> with Dictionary<Foo.ID, Foo>.

By the way, when Swift value objects came along and it became so simple to implement Equatable by selecting just the identifier, I fell for this exact trap. There’s still code in my apps that abuses the == operator when I should’ve compared their identifiers. So thanks to Helge for bringing clarity by talking about the more recent addition of Identifiable to the Swift language a bit more!