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?
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
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 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
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?
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.
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?
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
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:
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
Quite often, we have a few use cases working with similar objects:
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).
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.
Not let's look at the call site:
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
When applied to our base type
the call site can be simplified to
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
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.
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.
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:
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
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
actual feature. Its implementation just delegates the call to our standard
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.