Angular data-provider services: in-memory data cache
Why in-memory data cache?
In modern web applications, we are commonly using APIs for getting data. From time to time, we are facing a situation, where we need to pull data from one endpoint multiple times.
This can lead towards redundancy, especially when we are sure that the requested data didn’t change on the backend. To avoid such situation, we can build (within our data-pulling service) a simple in-memory cache.
That way, we will reduce the number of calls made to the API, so the server will need to handle less requests, and also browser won’t be forced to make to many requests, leaving more resources for other stuff.
Let’s see how we can build simple in-memory cache in a Angular application.
Data provider service for requesting API data.
One of key elements of Angular applications (next to the components) are services. In simple words, service is a class that can be a singleton, meaning that only one instance of this class will be created during applications lifecycle (so as long as the user will not refresh the page). This gives us the opportunity, to save data in service, and make it available for components.
Angular gives us a lot of flexibility in the way we can structure code. In my code, I often divide all services into types, based on what is their main responsibility. That division is just a convention, so it means its not forced by the framework , it is just the way i choose to do, as its convenient for me.
The type of service with the main goal for communicating with data API is data-provider. I give those services a specific name, that contains of: data-related-name + data-provider.service postfix. It simply looks like that:
Such service, except from communication with the API, can also supply a our code with simple caching mechanism for received data. This part of our service, will be the simple in-memory cache.
Let’s try to imagine an application, that will need to pull data at the same time from the same endpoint. Such situation can happen, for example, when we have a list of some objects received from an API with one data-call, and each of those objects contains reference to other API endpoints, for some details data or related object.
Data flow would look like that:
- Request data from first-endpoint: list of objects-A is received
- Each of received objects-A contains a reference to object-B from second-endpoint
- For each of received objects-A, request detail data by making a call to objects-B second-endpoint
We can now see, that in case when more than one of objects-A contains the same reference to object-B, the same data call will be made. And this is a model example where services in-memory cache is handy.
Use case example
Our example application shall displays list of football players with the football clubs they currently play for.
List of API endpoints that shall be used:
players/: returns list of football players objects
club/<id>: returns details data about the club
So as we see in the players-mock.json, there are 2 players from the same club (id: 1).
Let’s consider such components structure:
We have 2 components with associated models and data-providers:
players.component.ts: displays list of players (with
*ngFordirective in template)
players.model.ts: used for wrapping incoming data in a
players.data-provider.service.ts: pull players data from API (list of players)
club.component.ts: displays given club name in
club.model.ts: used for wrapping incoming data in a
club.data-provider.service.ts: pull club data from API
If we would create
club.data-provider.service as regular data-provider service (without in-memory cache), and just used it in
club.component, we would need to pull the same data twice for players 1 and 2, as they are from the same club.
Lets see how regular service would look, and how we can improve it with a in-memory cache.
Regular data-provider service
This service contains one public method:
getClub(id). This method is used by
getClub(id)returns an observable with the usage of
fetchClub(id)is making a
httpcall, and returns its observable, but wraps it with a
Clubclass by using
- nothing more is done, simple service for getting data from an endpoint
In our case,
players.component would create 3
club.components, one for each of received players. As
club.component makes a
getClub(id) data call, we would end up with 3 calls made to the API.
Two of those would be exactly the same:
In-memory caching data-provider service
As we don’t want to make the same data calls, we need to add in-memory cache to
- Lets add private members:
clubCacheas empty object literals
observableCachewill store ongoing requests
clubCachewill store received data
Both of those caches are objects, were
ids of a club, and
Club class instances.
getClub(id) will return an observable, just like before, but in bit more sophisticated way.
clubCachecontains requested club data (checking by its
id), we shall return observable of this
observableCachecurrently contains requested club data (again, checking by its
id), we shall return cached observable
- if requested club is not in the
observableCache, we shall create new key
observableCache(according to requested club
id) and assign an observable returned
fetchClub(id)as its value
- finally, we return cached observable
fetchClub(id) does exactly the same stuff as in regular.data-provider, with a small difference: we make the returned observable shareable by chaining
.share() method at the end.
This will allow multiple
subscribers to share the source (so if the call is in progress, new call will not be made - find out more about
share() in the rxjs docs).
We will have 2
subscribers on the
observableCache with the
id 1, so this comes handy.
mapClub() got changed into
mapCachedClub(). That’s just a name change, so its more self-explanatory. Except that, we also make a bit more sophisticated stuff than just wrapping incoming data with
- First, as the request for this data ended, we clear this observable data from
- Second, we shall create key in
clubCache(according to requested club
id) and assign an
Clubinstance as its value (based on incoming data)
- Finally, we return new
You can check out full code of
club.data-provider.service with in-memory cache:
players.component would create 3
club.components, one for each of received players. and
club.component makes a
getClub(id) data call, we would end up this time with 2 calls made to the API. One for club: 1, and one for club: 2.
When to use
Given example is not very sophisticated. However it gives an overview of how you can build in-memory caching mechanism directly in data-providers. Of course there are other, more complex and also more powerful ways of caching requests data (based on etag, local-storage etc.), but this solution is simple, requires no external libraries nor Api improvements. It also has its downsides, like handling cases when once pulled data changes in the API, but shall be a good starting point towards more complex solutions for catching once requested data.
I hope you enjoyed reading this article, and also hope it will help you level up your skills. If you want to see a follow up on this topic, (as well as other front-end stuff) don’t forget to follow me on Medium. In next article, I will show you how to improve this solution with more sophisticated rxjs methods, so that we will relay on one cache. Thx for your time.
As promised, article on improving cache with some more sophisticated methods can be found here: https://medium.com/@garfunkel61/rxjs-data-cache-7e1b7cb4c8f3