Repositories and Domain Models in Swift & Kotlin

Eric Silverberg
Perry Street Software Engineering
4 min readDec 18, 2020

What comes after an API response?

This repository could probably use a refactoring, but those domain models sure look tasty. Photo by Marc Noorman on Unsplash

APIs are like the engines of apps — every app has them, and they provide the fuel that makes the app work. But what happens after an API call completes? How is the data managed, munged, and manipulated before it is (ultimately) rendered in a View on iOS or Android?

***

Perry Street Software has been publishing apps since 2010, and in this series of blog posts, we introduce the Clean MVVM architecture for consistent cross-platform code in Swift and Kotlin.

The center of our abstraction — Repositories and Domain Models — is the part that should also be most closely aligned from a code perspective. In our Clean architecture diagram, Repository and Domain Model classes are represented by yellow 🟡 .

Both classes should be written in pure Java/Kotlin/Swift, and not have any knowledge of views, view models, or device and platform-specific implementations. Ideally these classes can be declared in Swift Packages or Java libraries and not the main app itself; that way tests can be run without an emulator.

Critically, Repositories have API classes injected. Repositories provide an abstraction to a local or remote API and map the responses from that API.

The code on both platforms, closely aligned

Models

Coming as no surprise, models are simple data structures used throughout our application (e.g. a Profile). Model objects are the only part of the inner layer that can be exposed to the outer layer (Views and APIs). Repositories map the API responses (probably JSON) into model objects.

On iOS, models should typically be defined as Structs; on Android, sealed data classes. Models should be immutable, and have no inputs beyond the constructor. Parsing of models is done via Codable on iOS or something like Moshi on Android.

Models should expose only primitive types (Strings, Ints, Doubles, Enums), and should not have any dependencies.

Repositories

Repositories are like an ORM that live on top of databases and network APIs. According to Google, you can consider repositories to be mediators between different data sources, such as persistent models, web services, and caches. Eric Evans, in his book “Domain Driven Design”, describes repositories as having the following advantages:

“They present clients with a simple model for obtaining persistent objects and managing their life cycle.”

“They decouple application and domain design from persistence technology, multiple database strategies, or even multiple data sources.”

“They communicate design decisions about object access.”

“They allow easy substitution of a dummy implementation, for use in testing (typically using an in-memory collection)”

The key to implementing repositories successfully is to ensure we are using the right Reactive primitives when exposing various types.

Most of your architectural time will be spent writing and testing Repositories.

The three essential operations on Repositories

The table below recommends primitives when accessing properties, i.e. values that should be always set and may change over time; retrieving values, either from the network or from a local store; and modifying state, such as a create, update or delete operation.

When accessing configuration parameters, typically stored in a PrefsStore (UserDefaults on iOS; SharedPreferences on Android), we allow ourselves to expose the Boolean or Int value directly; otherwise all else is a Reactive primitive!

Example: MatchRepository

To continue our example of a swipe-based matching feature, consider a MatchRepository:

In this example, you can see we expose the boolean values from our PSSPrefsStoreImplementing/IPrefsStore directly. Otherwise, the operation that goes out to the network for some data — getMatchStack — returns a SignalProducer<MatchStack, Error>/Single<MatchStack> All other methods that record or change state, such as adding or deleting a rating, return SignalProducer<Void, Error>/Completable.

Next up

We have reached the center of the Clean MVVM architecture. From here, thanks to Dependency injection, we have references to outer-layer API interfaces/protocols, which we will explore next.

More in the series

Other series you might like

Clean API Architecture (2021)
Classes, execution patterns, and abstractions when building a modern API endpoint.

Android Activity Lifecycle considered harmful (2021)
Android process death, unexplainable NullPointerExceptions, and the MVVM lifecycle you need right now

About the author

Eric Silverberg is the CEO and founder of Perry Street Software, publisher of two of the world’s largest LGBTQ+ dating apps on iOS and Android.

--

--