Clean Architecture — Functional Style Use Case Composition with RxJava/Kotlin

Legoville — A lego town!

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.

Purpose

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 composition.

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 User(or an Error). Composing 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 UseCases/Functions.

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 aUseCase as a specification and have no implementation details. (See the example below to understand these in more detail).

  1. A UseCase should be a concrete final class.
  2. A UseCase class name should be suffixed with UseCase.
  3. A UseCase should have a single public final method called execute().
  4. The execute() method should take a single parameter called Request. A Request is a data class (for example GetUserRequest(val userId: String)). All properties of a Request are val(read only).
  5. The execute() method should always return a deferrable computation (In this case, RxJava).
  6. The execute() method should return a Result<T>. A Result is a Success or an Error. I only use Observable<Result<T>> or Flowable<Result<T>> as return types. Maybe<T> and Single<T> are handled by the Result return type which can hold errors.
  7. A UseCase should not specify any custom annotations. Such details can be handled at the implementation level.
  8. The UseCase constructor should be injected only with other UseCases (see points 9 and 10). This is composition wherein we build a complex UseCase by composing other UseCases.
  9. 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 UseCase is still reusable and makes sense as an independent function.
  10. If no UseCase exists that provides the required data, a new UseCase is created that is injected with a single Repository or a Service interface. This typically happens for low level UseCases which fetch/save data. (For example GetUserByIdUseCase(val userRepository: UserRepository).
  11. The execute() method should not use RxJava operators that switch threads (subscribeOn(), observeOn(), etc.) or implicitly run on their own Schedulers (Observable.interval(), Observable.timer(), etc.).
  12. The execute() method makes no assumptions about the number of items emitted by injected UseCases.
  13. A UseCase is preferred to be a single chain of RxJava operators.
  14. Since the execute() method returns an Observable<Result<T>>, a UseCase makes no assumptions that any injected UseCase would specifically return a Success or an Error.
  15. A UseCase is immutable.
  16. A UseCase is stateless.

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!)

What does this code do?

If you guessed that this is a ticket booking feature with some validation and analytics, you’re awesome! :)

Key Takeaways

  1. The code through its variable, class and function names explains its purpose.
  2. Each of lines 9–13 can return an Observable.error() thereby aborting the flow and jumping to line 14. A UseCase should never trigger onError() on its client. All errors are converted to Result.OnError by line 14.
  3. A UseCase is 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.
  4. Helpful converters (listed below) like Result.toDataObservable and Throwable.toErrorResult allow easy conversion between Result, Throwable and the actual data type viz. Ticket.
  5. checkDoubleBooking and isSeatBooked can be extracted into a new UseCase called ValidateTicketBookingUseCase if required.
  6. A new validation step can be easily added.
  7. Modifying the business logic of an existing step does not impact this UseCase.
  8. Dependencies can be injected using a DI framework if required. Most DI frameworks work with @Inject.
  9. This UseCase can 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 interface.

A low level UseCase

Key Takeaways

  1. Uses a Service to perform I/O. The UseCase itself is unaware of it.
  2. Caching, threading, etc. are not part of the UseCase logic.
  3. Errors are defined at UseCase level and hence act as documentation.
  4. Services or Repositories can use Single<T> or Maybe<T>. It is preferred to convert these to anObservable<Result<T>> for easy composition.

RxJava Composition Helpers

These functions greatly help in composing RxJava streams. Hope you find them useful.

Class and functions for easy composition of RxJava streams

So why compose?

  1. A function should do one thing. This helps in writing robust, testable code. A UseCase here acts like a function.
  2. 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.
  3. Since a UseCase is an independent unit of logic, almost no context is required to understand it. This greatly simplifies finding and debugging business logic errors.
  4. UseCase composition 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 :)