Solving ambiguity and increasing performance with the Promise Type
We recently ran into an issue at Standard Bank Engineering where we were running the same service calls concurrently. With a large number of users using the app at any given time, any expensive and unnecessary service calls can put tremendous pressure on your backend systems as well as giving the perception of the app being slow.
As always you can follow along with the following playground file provided.
Describing the initial problem
We have a number of different feature teams. If we need data or functionality from a specific team, the team provides a public facing interface. Other feature teams who consume the interface don’t care about the implementation. Let’s take a look at simple example.
And this would be an example of a public facing interface.
Now all service calls should be cached, so let us write a quick interface and implementation for caching a user.
And finally the private implementation for the repository implementing the public interface.
Let’s step back and explain the above in more detail.
- The implementation checks the cache to see if we have a current user, if a user is found the user is returned.
- If the user is not found, we trigger a service call to fetch the user. We do all service calls on the background thread, preventing anyone from locking the main thread. If a service call is done on the main thread, we throw a fatal runtime error.
- Once a user has been fetched, we populate the cache.
You can test the above with the following snippet.
Now there are three major problems with this approach:
- There is no way to determine if a service call has been triggered and has not yet returned. Therefore, we can have multiple service calls happening before the cache has been populated.
- If we look at the interface, we have no idea that we have to run the function on the background thread. Yes the app will throw a fatal error at runtime, but we also shouldn’t have to look at the implementation.
- Finally we might accidentally call
getCurrentUser()on the main thread and not crash, because the cache has already been populated.
These are three significant problems that we need to solve for.
Solving the problem by using a Promise
Before we start with our solution, it is important to mention the Result Type. I mention it in quite a few of my articles, but basically an app can be in one of two states, either a success state or in a failure state. Wikipedia sums it up quite nicely.
Result Type provides an elegant way of handling errors, without resorting to exception handling; when a function that may fail returns a result type, the programmer is forced to consider success or failure paths, before getting access to the expected result; this eliminates the possibility of an erroneous programmer assumption.
Now there is another state a program can be in. And this is where the Promise Type comes in. Wikipedia, once again, sums up Futures and Promises quite nicely.
Promise and Futures describe an object that acts as a proxy for a result that is initially unknown, usually because the computation of its value is yet incomplete.
Basically you can think of a Promise as a Future Result. So correcting my previous statement, a program can actually be in one of three states:
- Success state
- Failure state
- Waiting / Unknown state
There are quite a few frameworks and libraries that have impemented the Promise or Future Type, but I am specifically using Google’s. By updating our interface from:
We remove all ambiguity. We longer need to look at the implementation to determine if we have to run the function on the background thread. We have a promise that we will eventually get a value. The contract states explicitly: do what you need to do in the meantime while we try to fulfil your request. If we cannot fulfil your request, we will let you know why so you can handle accordingly. Otherwise your result will arrive sometime in the future.
Refactoring our previous implementation
We can update our implementation for getting a current user to the following.
But we still need to cache our results.
But instead of caching our results, can’t we simply cache the Promise? By updating our cache protocol and implementation with the following.
We can now update our repository to cache our Promise and prevent unnecessary service calls being triggered without using anything fancy or complicated.
Finally if the Promise fails we catch it and clear the cache.
I have set up a little playground, to illustrate visually what we have achieved. We have two tabs, independent of each other, that require the same set of data. Both tabs are using the same interface to call the same function at the same time.
And if the service call fails, we get the following.
Test the above out with all the code using the following playground file.
Promise is a Functor and a Monad
Google has an excellent Swift Promise Framework that is compatible with Swift and Objective C. It is the same library I use in my playground example. So if you are comfortable using
map with Arrays, Optionals, and / or the Result Type, you will be happy to know that the Promise Type follows the same laws allowing you compose functions just as you were previously. Let’s talk more about that next time though.