From one God Interactor to focused Use Cases

Introduction

Juan Peretti
5 min readJul 11, 2020

This article tells the history of how I decided to go from a single interactor per screen approach to a UseCase based architecture for the domain layer of my application. MoviesPreview is an application that allows a user to browse the data exposed by The Movie DB API to see different information about movies, actors/actresses, and some other features. The full code of the project can be found in this Github repository.

The Journey

I read about Clean Architecture in Android projects like 6 years ago and my initial thought was: this is definitely the way we should architect standard Android applications. But, to be honest, I always struggle with two key concepts of the Architecture: Interactors and UseCases.

Are they the same thing? What is the difference between them? Should they contain the business rules of the application? These are some of the questions I always had in my head and every time I tried to answer, I failed to find a convincing answer.

The architecture ended up with a concept of Interactor that was complex and an implementation that was super complex, having really large classes with more responsibilities of what they should have. Even more: I tried to apply a reactive approach in this layer, making use of LiveData and Transformations to adapt the states of the domain layer to the states of the UI layer.

Conclusion: it was impossible to re-use the logic in interactors and really difficult to read the code in them to understand what was doing to implement the responsibility it was given to it.

The Evolution

The Interactor concept was not working well for me. It wasn’t scaling and it was making it difficult to introduce changes. The evolution I came up with was: UseCases. Yes, nothing fancy. But the key to having the mental breakthrough that helped me to clean up the domain layer was that I defined what a UseCase is in my architecture.

I defined a UseCase as a component that could have one of two basic responsibilities:

  • Query the system to fetch a specific state of the application.
  • Interact with the system to produce a change in the state of the application.

In the definition, the system is no other than the data layer of the application, since it is the layer that is used to maintain the state of the application — independently of where that data layer is storing the data.

This way I replaced the God interactors for focused UseCase. Each UseCase has only one responsibility (following the single responsibility pattern) and the number of lines in each file got reduced significantly. It also helped me to clarify the UI layer of the application.

UseCase implementation

On a very high level, the initial implementation of my UseCase had the following public API:

fun execute(param: AParameterType): ADomainEntity

This means that a UseCase can receive a parameter (or several when needed) and it would produce an object from the domains entities (or in some cases a boolean return to indicate that a state change was successful).

The first problem I spotted here was the return type: it could be an actual object if the execution of the UseCase was successful or a null value if the execution failed. This didn’t scale well in code since I had to check for null on every result that I get from the domain layer.

Even more, this initial implementation lacked a way to identify the source of failure: if the result was null, did the UseCase failed because of a network issue? Did it fail because of an error in the data layer? impossible to know with this implementation.

So, the problem was basically that I had to represent the different options I could get from a UseCase execution. The solution came to me in my current gig: another developer recommended we start using a representation of optional results called Try and that did the trick to me. I immediately adopted the concept and adapted the UseCases public API to be:

fun execute(param: AParameterType): Try<ADomainEntity>

That way it was far much easier and readable to map the results from the domain layer:

val result = useCase.execute()
when (result) {
is Try.Success -> processResult(result.value)
is Try.Failure -> processFailure(result.cause)
}
///private fun processFailure(failure: Try.FailureCause) {
when (failure) {
is Try.FailureCause.NoConnectivity -> showNoConnectivity()
is Try.FailureCause.Unknown -> showGenericError()
}

The failure causes are mapped usingsealed classes (Kotlin):

sealed class FailureCause {
object NoConnectivity : FailureCause()
object UserNotLogged : FailureCause()
object Unknown : FailureCause()
}

Note that I decided to handle only these type of errors because those are the relevant ones for my application, but I could extend FailureCause to map as many errors as I would want to handle in the future.

Finally, since I’m using Kotlin coroutines to handle long-term operations that could potentially block the UI, all the data layer access methods (all methods in my repositories) are marked as suspend . This forced me to mark the execute() methods of each UseCase as suspended too, ending up with:

suspend fun execute(param: AParameterType): Try<ADomainEntity>

That is the public API that all my UseCases are exposing to the upper layers.

A note about coroutines and suspended functions

As I mentioned above and in the post that explains the data layer of MoviesPreview all the repository, and UseCase methods are marked as suspend in order to leverage the usage of Kotlin coroutines.

Using coroutines this way is exposing a weakness of my architecture: the responsibility of handling the threading mechanism is given to different layers of the application. To be absolutely clear, this responsibility should be given to a single layer of the application (UI, or domain, or data) and not leaked to all of them.

To me, the obvious place to have this responsibility is the UI layer, since that layer is the one that is absolutely dependant on the platform. The domain and the data layer should be ‘portable’ to other platforms without the need for modification.

This is a compromise I did in the definition of the architecture in order to maintain a clean implementation in code.

--

--