Modelling the domain layer using Composable Use Cases
Some time ago we had a problem inside the Memrise Android codebase with our domain layer. Repositories and Use Cases were heavily used but there weren’t clear guidelines on what they were supposed to do and not do. It was common to find them being used in combination, which in reality meant we didn’t have Repositories at all but Use Cases with direct access to our Data Sources.
With this in mind, we started looking for a brighter future and thinking about what was right and what wasn’t:
- Repositories with other Repositories as dependencies ❌
Repositories should only depend on their Data Sources (i.ememory
,persistence
orapi
). Whenever we have a Repository doing more than this, we should treat it as a red flag and as a symptom that what we want is a Use Case. - Repositories with Use Cases as dependencies ❌
Also bad. If our Repository has a Use Case as a dependency… Well, what we’ve got is a Use Case that is acting both as a Use Case and as a Repository! But we want our classes to have a single responsibility. - Use Cases with Repositories as dependencies ✅
Yes! This is the classic showcase of a Use Case. Calls a Repository to fetch the data, not caring how the data was fetched or where it’s coming from (api? persistence? it doesn’t matter). Then, it implements the domain/business logic before passing it back to the presentation layer, where it will be transformed into some sort ofViewState
to be consumed by views. - Use Cases with Use Cases as dependencies ❓
This one had us thinking since we had some questions, such as:
- Will this lead to circular dependencies?
- Will this work well with modularisation or will we end up with a massivecore
module?
- What about when a particular screen needs to tweak the logic, will that detail leak to other callers?
- As Pablo Costa pointed out (thanks 🙏) it violates the Dependency Rule
The real answer for Use Cases within Use Cases, or in other words, Composable Use Cases is that…THEY’RE GREAT! ✅✅✅
Let’s see some examples to see why they’re great and get a grasp of them.
Use Cases
Before we start seeing some code, it’s worth describing some basic rules we normally follow when creating Use Cases:
- Ideally, they’ve got a single function (i.e
invoke()
) that receives a parameter and returns something. - No threading details on them, these live in the Presentation layer (they’ll normally be run asynchronously).
- Chaining using Rx or any other framework of your liking.
- Dependencies are injected using Dependency Injection.
- They don’t hold state.
NOTE: all code below is inspired in real implementations but simplified
Low-level Use Cases
Low-level Use Cases won’t do much. They are a single function that takes a parameter, calls a Repository, and returns some data. For example:
Not much going on here: a single dependency, the Repository. It calls the getEnrolledCourse
method, if found it will be returned, otherwise, it throws an error. Can't get more low-level than this!
Simple Composable Use Cases
Now, let’s go one step further. We’ve got a screen that shows if an enrolled course is available for offline mode. We can do this without writing much code by composing a few low-level Use Cases, including the one we presented above. It would be something like this:
Delegating Complexity to other Use Cases
Use Cases can get very complicated very quickly. When that happens, it’s important to apply a bit of Single-Responsibility-Principle and delegate to other Use Cases. At Memrise we’ve got a component called the Single Continue Button
(SCB from now on) that it's a great example of this. The SCB is a button that can appear on multiple screens that will launch a new session when clicked. Depending on the screen where the user is at (dashboard
, level
, course
, end of session
, etc) and the user's progress, the session type will be different.
Instead of having all this logic in a massive Use Case or several Use Cases as entry points, we can have the main entry point Use Case that checks the provided payload and delegates to the right Use Case:
Chaining set of actions Use Cases
Somehow similar to the previous example, we can have a Use Case that orchestrates the chaining/order of different actions. This will not only help to visualize logic easily, but it’ll make testing easier, as it moves away from implementation details. Fetching the Learnables for a particular session is a good example of it:
Summing up
Composable Use Cases are a great way to effectively model the domain logic of our software, abstracting away our Repositories and data layers. On top of this, they’re a very scalable solution as the more Use Cases we write, the more we can re-use and quickly ship new features to our users!