Clean Architecture — Functional Style Use Case Composition with RxJava/Kotlin
Imagine you’ve been assigned to a new project and this is the first meeting where the first thing the project manager says is
We want to give our users the ability to book a movie ticket from the app. We’ll validate the booking, update the database on success, send push notifications on an I/O thread and cache for better performance. Also, we should log and send analytics.
Weird right?! Business requirements never talk about how to get something done.
This is probably what your code looks like to someone else (or you after 6 months) when they’re reading it. The most important part is buried somewhere in a pile of functions and variables that had to be added for error handling, logging, threading, etc.
Clean Architecture mentions a
UseCase as a specification of business rules. These rules are implementation independent. In other words they don’t specify usages/features of platform frameworks like Spring or Android, threading requirements, caching, analytics, etc. They purely define what steps are required to complete a business flow.
However, such business flows are complex and involve many steps. Some of these steps can be reusable. Moreover, the result of a step can determine if the next step needs to be executed, skipped or the entire flow needs to be aborted.
In this article, I share my techniques of handling such flows in a more streamlined manner through
UseCase as a Function
Loosely speaking, a
UseCase can be thought of as a function
T -> R that maps some
T to some
R. For example
GetUserByIdUseCase takes a
userId and returns a
UseCases then becomes similar to composing functions. Complex
UseCases are built from simpler ones by combining them in various ways.
It is worth noting that the input of this
UseCase function is a combination of the
Request parameter of the
execute() function and all the injected
The UseCase Contract (Conventions)
In order to stay ‘functionally pure’, I use the following conventions in writing a
UseCase. These conventions also help in keeping a
UseCase as a specification and have no implementation details. (See the example below to understand these in more detail).
UseCaseshould be a concrete final
UseCaseclass name should be suffixed with
UseCaseshould have a single public final method called
execute()method should take a single parameter called
data class(for example
GetUserRequest(val userId: String)). All properties of a
execute()method should always return a deferrable computation (In this case, RxJava).
execute()method should return a
Error. I only use
Flowable<Result<T>>as return types.
Single<T>are handled by the
Resultreturn type which can hold errors.
UseCaseshould not specify any custom annotations. Such details can be handled at the implementation level.
UseCaseconstructor should be injected only with other
UseCases (see points 9 and 10). This is composition wherein we build a complex
UseCaseby composing other
- If the number of
UseCases being injected grows, some of them can be extracted and combined into a new
UseCase. Such refactoring should ensure that each
UseCaseis still reusable and makes sense as an independent function.
- If no
UseCaseexists that provides the required data, a new
UseCaseis created that is injected with a single
Serviceinterface. This typically happens for low level
UseCases which fetch/save data. (For example
GetUserByIdUseCase(val userRepository: UserRepository).
execute()method should not use RxJava operators that switch threads (
observeOn(), etc.) or implicitly run on their own
execute()method makes no assumptions about the number of items emitted by injected
UseCaseis preferred to be a single chain of RxJava operators.
- Since the
execute()method returns an
UseCasemakes no assumptions that any injected
UseCasewould specifically return a
UseCase Composition Example
Rather than describing the example, I’ll let you look at the code and try to figure out what’s happening here (after all, that’s how software development works anyways!)
If you guessed that this is a ticket booking feature with some validation and analytics, you’re awesome! :)
- The code through its variable, class and function names explains its purpose.
- Each of lines 9–13 can return an
Observable.error()thereby aborting the flow and jumping to line 14. A
UseCaseshould never trigger
onError()on its client. All errors are converted to
Result.OnErrorby line 14.
UseCaseis preferred to be a single chain of RxJava operators as shown here. If you find yourself getting into complex RxJava chains, consider refactoring your code into separate
UseCases and compose them to simplify your logic.
- Helpful converters (listed below) like
Throwable.toErrorResultallow easy conversion between
Throwableand the actual data type viz.
isSeatBookedcan be extracted into a new
- A new validation step can be easily added.
- Modifying the business logic of an existing step does not impact this
- Dependencies can be injected using a DI framework if required. Most DI frameworks work with
UseCasecan be tested by mocking injected
UseCases or by using fakes.
Low level UseCase
Let’s look at a
UseCase that deals with I/O. This
UseCase is injected with an
- Uses a
Serviceto perform I/O. The
UseCaseitself is unaware of it.
- Caching, threading, etc. are not part of the
- Errors are defined at
UseCaselevel and hence act as documentation.
Maybe<T>. It is preferred to convert these to an
Observable<Result<T>>for easy composition.
RxJava Composition Helpers
These functions greatly help in composing RxJava streams. Hope you find them useful.
So why compose?
- A function should do one thing. This helps in writing robust, testable code. A
UseCasehere acts like a function.
- A business logic step is represented by a
UseCase. Such steps can be modified without impacting the ‘business flow’. Steps can even be swapped for different ones or skipped conditionally. New steps can be added easily.
UseCases are pluggable.
- Since a
UseCaseis an independent unit of logic, almost no context is required to understand it. This greatly simplifies finding and debugging business logic errors.
UseCasecomposition is independent of software architecture and implementation details. This allows to choose and upgrade backend and frontend frameworks without impacting business logic.
That’s it for now. Thanks and hope you’ve enjoyed reading this. Do let me know what you think and happy coding :)