Application domain modeling
Tags: architecture, kotlin, swift, multiplatform
Published: 2022-10-06 10:36:28

Software projects are about telling computers what people want to do. Programmers are the ones who speak the computer language, but ordinary people aren't predestined to think and communicate the way a computer does. They are usually good at telling what they want to achieve and what is the purpose. Then it's the programmer's task to translate the purpose into computer language.

This arrangement leads to an unintended limitation. When a person needs to tell something to a computer, they need a programmer. When people need information about computer behavior, they need to ask a programmer. Programmers are a bottleneck in this process.

But what if we can design the project so the code is understandable even for ordinary people?


## Enter the domain

A domain is a part of every project that does the important things. The business logic resides there together with industry rules and policies. The domain must be articulated unambiguously for everyone.

The domain shouldn't be too technical. There should not be transactions, exceptions, wrappers, or gateways. Ordinary people do not understand these terms but need to understand the domain. Although programmers understand these technicalities, they are ambiguous, and two programmers may imagine a different mechanism under these terms.

The domain should be full of real-world objects. We should see purchase orders, money amounts, other people, or actual goods. These objects can be virtual, like card transactions, but need to be recognizable to everyone.

Kotlin exhorts for readable code. This is one of its main characteristics. And we can use it to model our domain.

The real-world objects are usually modeled as `data class` with read-only properties. Any modification needs to be done by a generated `copy()` function that prevents unintended state updates or inconsistencies.

Some objects or data items like Social Security Numbers are so simple that one may tend to represent them as a primitive type like `String` or `Int`. This is good for program efficiency, but a person may unintentionally swap it with some other information of the same primitive type and introduce a severe error. To overcome this, we may introduce a `data class` to wrap the information and give it a unique type. This will work, but its overhead may consume an unacceptable amount of resources when used at a larger scale. Kotlin `value class` is a great fit for such use cases. It assigns a unique type to a simple data item while still keeping its memory footprint small.


## Domain actions

Domain objects are important, but they are useless unless we can take action with them. A purchase order is to be placed or canceled. A parcel with a new pair of shoes is to be shipped or claimed. And we need to model these actions as well.

The first thing is the name. `Action` in programming is quite an overused, and generic term and it does not work well for us. Some teams name them `Interactor` others use the term `UseCase`, or `Actor`. We will use the term `UseCase` for the rest of this post.

A use case is defined as a single function with explicit input and output. Use cases have no state and can be called multiple times. Let's try to define them in Kotlin.

It would be nice if a `UseCase` may have a single input and single output so we can define a base type for all use cases with inputs and outputs defined as generics. But in the real world there are actions that may have more inputs, so this is not feasible, is it?

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u163xtjgfu29erx3t4dw.png)

This approach will work technically, but it does not tell the whole story for people unfamiliar to the context.

Introducing a specific type for use case input will actually help to understand the context and works as an extension point for further feature evolution.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h93vgogibvpsmxc9gkyu.png)

Great, after using this approach for a while, you'll realize that some use cases have their input and output optional. That's fine, Kotlin has built-in nullability in the type system, doesn't it?


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ch0qqbgtzc6qkojjp8wq.png)

The use case finds all our company stores. We may want to get a list of all of them or to filter them by a given `Country`.

The definition of input as `null` on the use-site is not very understandable in this case. Also, it is hard to guess what `null` as returned result actually means. A little bit more modeling will fix this:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tjra0w8427vd9pf14ax5.png)

It seems to be a good idea to deny the value `null` as use case input or output at all. Let's introduce a base type and force the generics to be based on `Any` that effectively forbids `null`:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xqo3xav2dkw3ti1y68eo.png)


## Actions namespace

Quite often, we have a few use cases working with similar objects:

`SubmitPurchaseOrderUseCase`, `CancelPurchaseOrderUseCase`, …

These class types may quickly pollute the project's namespace and make it hard to navigate. We should try to group them somehow under a shared namespace. Kotlin doesn't have support for namespaces ([yet](https://youtrack.jetbrains.com/issue/KT-11968/Research-and-prototype-namespace-based-solution-for-statics-and-static-extensions)).

We can supersede it by grouping the use cases under a single `sealed interface` for this purpose. You can use `sealed class` or `object` as an alternative, but the memory footprint would be slightly bigger.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2r1a4q4bg02iiqm691sf.png)


## Kotlin call site

Not let's look at the call site:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/802texvhw9fhdg4hs3yy.png)

We can polish it with a little trick called Invoke operator. When a Kotlin function named `invoke()` is marked as `operator`, its invocation may be replaced by the use of `()` operator.

When applied to our base type


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bgn4ch375q73uopssc86.png)

the call site can be simplified to


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k8q647bz2esz0owybjib.png)

Notice the use case instance name change, where the `UseCase` suffix has been omitted, and its invocation now looks like a standard function call. If it is used in an obvious context, we may shorten the use case instance variable name even more and call it simply `submit(purchaseOrder)`.


## Swift call site

Until now, we were only calling our use cases from Kotlin code. On Kotlin Multiplatform projects, it is common to share the domain logic between all targeted platforms. Let's check how a use case may be used in another language. We will take the example of a mobile application implemented both for Android in Kotlin and iOS in Swift.

First, let's try to call it the same way we do in Kotlin.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ixpw1m9p8pdukgzenrpc.png)

This code has two major problems. The invocation operator is unavailable, and the result is not of type `SubmitResult`. Let's start with the latter one.

_**Swift generics**_

Generics in swift are available for `class` only. A very similar thing for `interface` is called associated types, but its characteristic is incompatible with Kotlin generics.

This is why Kotlin Multiplatform has Swift generics support only for classes. To fix this in our project we need to define our base type as an `abstract class` instead of an `interface`. This is actually not an issue as its implementations are classes anyway:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wj529gd4u2mu0gkfvkqq.png)

_**Swift function invocation**_ 

Now let's fix the former problem with use case invocation. Swift language doesn't have an invoke operator like Kotlin. Instead, it has methods with special names, and one of them is `callAsFunction()` that can be called with the use of `()` symbol.

We may introduce this function to our use case base type but that may be confusing for use from Kotlin common code or Android code.

To make it available for iOS code only, we will provide different implementations of use case base type for Android and iOS using Kotlin's `expect`/`actual` feature. Its implementation just delegates the call to our standard `invoke()` function.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w3fk4m8o9opw0g8bbp2n.png)


## Living documentation

The domain is a part of every project that actually does the important things. When modeled properly, it suits so-called living documentation. It is understandable for any reader; anytime a logic is changed, the documentation also changes.

Anytime you find a piece of domain code that is not understandable, let's take a moment and polish it. Your future will be grateful to you.


Pavel Švéda
Twitter: @xsveda