Felipe Joglar

Blog
Wait, where is my domain?

Wait, where is my domain?

The domain layer is optional! This is a trend in the Android community now. But should it be? We are mostly writing an app for a business, and this business definitely operates in a specific domain. Let's find it.

Wait, where is my domain? cover
Photo by Daniel Lerman on Unsplash

There is a common misconception in the Android community about the domain layer. And this is for a strong reason. Google says in its architecture guides, that this layer is optional. Really.

The domain layer is an optional layer that sits between the UI and data layers.

So many programmers would normally think so. But is it really like that? Is the domain layer optional?

When did it all begin?

So, let’s think about how this optionality originated. Usually, when we talk about the domain layer, we think about use cases or interactors (at least in the Android world), as defined by Uncle Bob in its Clean Architecture blog post. Then, in an attempt to conform to a cleaner code, everything became a use case. Even a simple repository call got wrapped in a use case. Or formatting a date for presentation purposes!

After that, a smell started to arise. Use cases were everywhere. The codebases began to be full of useless use cases™️, which only encapsulated a call to another collaborator. The middleman anti-pattern was knocking at the door.

Now, we have a bunch of use cases that caused an anti-pattern. Our domain layer is then useless in many cases. Let’s remove all those use cases that do nothing, call the repository directly from the presentation layer, and make it optional. And here we are.

The domain layer is mandatory

The problem is that we have built these decisions based on wrong assumptions. First, the domain layer isn’t built exclusively from use cases. And second, we probably already have this layer, but it is spread across other layers. We should group it and take it to the place it deserves.

Every app, or almost every app, is built for a concrete business domain. It can be banking, communication, gaming, playing media, etc. Those apps have business logic, and not all business logic consists of complex operations and data transformations that can be encapsulated in a single reusable component. It could be as simple as loading a feed from a social network or getting the comments for a specific feed item.

However, the domain is not only suited for behavior. Some data structures are part of the domain. Following the example above, a FeedItem or a Comment, are things everyone can understand and know about in your business, from programmers to marketing to HR, and thus they are part of the domain.

Finding our missing domain

So let’s create a domain for a simple feature. We will set a couple of constraints to adhere to what we said above:

  • We can’t have useless use cases. We can’t have a use case its only work is to delegate the call to a collaborator.
  • We can’t access the data layer directly from the UI. So the ViewModel can’t access the Repository directly. That means that we should go through the domain.

First, we will define the goal of the feature we have on our hands. Following the above examples, we will implement a feature to show a social media feed to the user. So they can stay up to date with their friends and family.

But what is a feed in the scope of our feature? We will start with a basic data structure to hold all the data — this is a simple example, a full domain may look quite different.

data class FeedItem(
    val id: Uuid,
    val title: String,
    val body: String?,
    val imageUrl: String,
    val createdAt: Instant,
    val author: Author,
) {
    data class Author(
        val id: Uuid,
        val name: String,
        val avatarUrl: String,
    )
}

Great. This structure will be part of our domain, it is not only data, it is a domain entity representing a business concept.

Now we need a way to get this data for our users, so how do we go around it if we can’t call the repository from either the ViewModel or a UseCase? What we need is a contract. Our domain needs a way to get this information, so we can define an interface to act as a contract for the business needs.

interface FeedLoader {
    fun load(): Flow<List<FeedItem>>
}

And that’s it. We have the domain for our feature defined. Now, it is time to wire this up together with the rest of the layers.

Wiring up our domain

Now that we have the domain defined. We still miss the real thing, the concrete implementation of the contract.

For this example, the implementation will live in the data layer. There, we will have a Repository that will be in charge of getting the data from wherever it needs to be retrieved. It is not a domain responsibility to choose or even know where the feed data is retrieved from, that responsibility is for the data layer.

class FeedRepository : FeedLoader {
    override fun load(): Flow<List<FeedItem>> {
        // Retrieve data
    }
}

The contract is now fulfilled by our data layer. It’s time to inject it into our presentation layer to make use of it.

We can’t access directly the FeedRepository from our ViewModel. It was one of our constraints. So, our ViewModel will depend on the domain contract, it’s all it needs: a way to get the data. It doesn’t care who provides or how this data is retrieved.

Here is where the composition root comes into play. Using dependency injection patterns, we can pass the concrete implementation to the ViewModel without coupling any layers. The application module (:app) usually takes this role, as it has a view over all the other modules composing the whole app, integrating them all.

The ViewModel will look like this:

class FeedViewModel(
    private val feedLoader: FeedLoader,
) : ViewModel() {
    fun getFeed() {
        viewModelScope.launch {
            feedLoader.load().collect { feed ->
                // Do your thing
            }
        }
    }
}

A simplified dependency diagram for this feature would be like this one:

arch-diagram.png

We can see that no arrows go from our domain to other layers. And, the data and presentation layers are also separated by the domain. So we are keeping the clean architecture dependency rule in place. Also, we have managed to comply with the given constraints.

We found it!

Now we have our domain in place. No useless use cases and no direct dependency between data and presentation layers.

It doesn’t mean that this is the only way to go. As you are operating on a team, yours may be a different approach, and that’s fine! At the end of the day, your team may have agreed upon other conventions, like having a use case for every interaction, or accessing the repository directly if there is no data manipulation in between. Whatever keeps the team aligned and moving forward is a good approach.

Domain modeling is a must-have skill. And, it is not as easy or trivial as this example above. The domain exists in almost all features in every app, and it is our work to do our best to define it and give it its place. It is not optional, a feature can have it or not, but it is not a matter of choice, it is what the feature asks for.

I’m using this approach in my personal and work projects, and it is working pretty well. It gives me and my team a unified solution. We use the contracts when there is not much data manipulation. We can build up proper use cases when required and encapsulate a set of operations at the domain level. And give us a set of understandable data structures to work with.