Localstorage design patterns and passive expiration

Posted by Eric Tsai on

At Grubhub we made the decision to rebuild the consumer web application with AngularJS.  We did this knowing that we no longer wanted to maintain our user’s application state on the server so we chose to utilize HTML5 localstorage.  This helped in reducing maintenance complexity in our frontend servers and allowed our web application to more closely follow native mobile app behavior.   This process was a great learning experience for us and so we wanted to share our insights in case you’re considering a similar approach in building great applications.

Layers of abstraction

The first thing we did was create an abstract layer on top of the native localStorage.  This allowed us to gracefully handle error states, parse different data types and add advanced features on top.  We had previously used a popular 3rd party plugin but eventually moved towards a custom solution (aptly named localStorageUtility) that would work best for our application’s specific needs.

We also enforced a design pattern where our developers were not allowed to directly inject localStorageUtility into our components/directives/controllers but instead referenced the stored object indirectly through a data service.  For example, if we had a stored restaurant object in localStorage we would retrieve it through restaurantService.getStoredRestaurant().   Not only does this mean that we manage the local storage key in one location only, but it also ensures that we get back our custom class and not a plain javascript object.  The flow and code snippets are noted below:

restaurant-service

function getStoredRestaurant() {
    return new Restaurant(localStorageUtility.get('restaurant'));
}

local-storage-utility

function get(key) {
    return JSON.parse(localStorage.getItem(key));
}

Formatting data

A benefit of having a localStorageUtility is the ability to accept any datatype and gracefully read/write it.  Native localstorage is a string-only key/value store, so it can be a bit taxing remembering exactly what you put in and pull out.  We got around this by leveraging JSON.stringify and JSON.parse, respectively, for writes and reads.  It will properly convert objects, numbers, dates and strings back and forth without any further intervention.

var aNumber = 12345;
localStorage.setItem('utilityTest', JSON.stringify(aNumber));
localStorage.setItem('nativeTest', aNumber);

expect(JSON.parse(localStorage.getItem('utilityTest'))).toBe(12345);
expect(localStorage.getItem("nativeTest')).toBe('12345');

Passive expiration

Unlike cookies, we cannot mark a key to expire at a certain time which means that we must manage that logic ourselves.  Once a key is written, it persists until the user clears their browser data or the application forcibly deletes it.  

That means if we wanted to automatically sign out a user that had not logged in after X amount of days, then this purging of data would not happen until the user were to visit the site again.  We’ve tried to play with using sessionStorage (purges the stored data after all tabs are closed) or actively deleting the data before the user left the application, but they both resulted in less than ideal user experiences.  Our solution was to store a last used timestamp of the last time localStorage was touched (get/set/delete).  Prior to this update, we would check if the timestamp exceeded X days.  If it did then we purged all localStorage data in order to protect our users.  

Tiered Expiration

You might have noticed that we only mentioned one timestamp for all the keys in localStorage.  Our original concept involved tagging each localStorage key with its individual expirationDate. Here’s an example: 

{
		'aKey': {
			expirationDate: 1479767511511,
			value: ‘this is the value of A key’
		},
		'bKey': {
			expirationDate: 1479767512220,
			value: ‘this is the value of B key’	
		}
	}

While this would allow very fine-tuned controls on what gets purged, it would get linearly more expensive based on the localStorage usage.  On every modification to localStorage we would be required to check every single key to validate against its expiration date. This can ultimately hurt application performance so weopted to go with a tiered localStorage solution using string prefixes.

Let’s say we wanted to maintain three tiers:

  • Level A purges after 3 hours of inactivity
  • Level B purges after 2 hours of inactivity
  • Level C purges after 1 hour of inactivity

We will also declare the following for each tier:

  • Level A must start with prefix “a-”
  • Level B must start with prefix “a-b-”
  • Level C must start with prefix “a-b-c-”

Whenever we set/get/delete from localStorage of that specified tier we would prepend its key value with its designated string prefix.  Now, if we find that localStorage hasn’t been touched in 2.5 hours, we would want to purge all level B and level C keys only.  This enables us to clean up all associated keys by iterating through the localStorage keys and deleting any keys that begin with “a-b-” since that would also encompass “a-b-c-”.  All level A keys would, however, remain intact though because they begin with “a-” only.  Here’s a handy diagram below:

Incompatible incognito

It’s important to note that localStorage and sessionStorage do not work on certain browsers in incognito mode.  The most notable one is the Safari browser on both MacOS and iOS devices.  This issue deserves its own blog post because this section wouldn’t do this major problem enough justice. Stay tuned!

To sum things up…

An application is only as strong as its foundation and, by following these rules, we were able to effectively build a consumer web application that serves hundreds of thousands of people every day.  To summarize, we created layers on top of the native localStorage to improve the development cycle & code maintainability, read/write multiple data types gracefully, and introduce tiered expiration.  

We are always testing new ways to meet the ever-evolving needs of our diners and are looking forward to sharing how the sausage gets made. (We also, like food puns). Stay tuned for more from grubhub bytes!