Angular data-provider services: in-memory data cache

Why in-memory data cache?

Keep once received data in a box.

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:

data-related-name.data-provider.service.ts

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.

Use case

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:

  1. Request data from first-endpoint: list of objects-A is received
  2. Each of received objects-A contains a reference to object-B from second-endpoint
  3. 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 *ngFor directive in template)
  • players.model.ts: used for wrapping incoming data in a Player class
  • players.data-provider.service.ts: pull players data from API (list of players)
  • club.component.ts : displays given club name in <span> element
  • club.model.ts: used for wrapping incoming data in a Club class
  • 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 club.component.ts.

  • getClub(id) returns an observable with the usage of fetchClub(id)
  • fetchClub(id) is making a http call, and returns its observable, but wraps it with a Club class by using mapClub(rawData)
  • 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: example-api/club/1

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 club.data-provider.service.

  • Lets add private members: observableCache and clubCache as empty object literals
  • observableCache will store ongoing requests
  • clubCache will store received data

Both of those caches are objects, were keys are ids of a club, and values are Club class instances.

getClub(id) will return an observable, just like before, but in bit more sophisticated way.

  • if clubCache contains requested club data (checking by its id), we shall return observable of this Club instance
  • if observableCache currently contains requested club data (again, checking by its id), we shall return cached observable
  • if requested club is not in the clubCache nor 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 Club class.

  • First, as the request for this data ended, we clear this observable data from observableCache
  • Second, we shall create key in clubCache (according to requested club id) and assign an Club instance as its value (based on incoming data)
  • Finally, we return newClub class instance

You can check out full code of club.data-provider.service with in-memory cache:

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

PS.

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.

PPS.

As promised, article on improving cache with some more sophisticated methods can be found here: https://medium.com/@garfunkel61/rxjs-data-cache-7e1b7cb4c8f3