g43ld

The simple yet powerful technique of Memoization

How memoization avoids repeated network work and turns expensive integration calls into fast cached lookups.

As a developer making integrations systems, the server programs I write at work generally make a lot of HTTP requests. But a single HTTP request involves a lot of steps in the background:

And this is only for the request, all these steps must be done in reverse for the response.

Moreover the API you are talking to surely have a rate limiter in place, so the request might take even longer.

In short, the less API calls you do, the better.

The situation

I had an issue with one of our integration at work, it took approximately 4 hours to complete its data synchronization. You can imagine it is waaaay too long and our customer began to raise questions.

So I read the code, run some tests and quickly figure out two things:

Now, I couldn't do anything about the 1st point. I wrote some email to our contact to check if that was normal but that's it. But the second point was less frustrating.

Memoization, what is this thing?

Well, it's a really simple but powerful optimization technique, it's a form of “dynamic” caching, belonging to the world of Dynamic Programming.

The idea is to store the result of an (expensive) operation and reuse that result if the same input(s) occurs again, during execution of the program.

This way you never do the same operation/calculation twice.

The caching is done at runtime, losing the cached data once the program has finished its execution.

In the program that was taking too long, for example, an API request was done for each Student ID that we fetched previously.
As there was no caching in place, one student might be fetched 2, 10, 50 times! By applying this technique, each student would be fetched one time and one time only.

The implementation is quite simple (here's a TypeScript example kept as minimal as possible for the sake of conciseness):

async getStudent(studentId: string): Promise<Student> {
            try {
                // Read cache and return. 
                // The benefits of memoization happens here.
                if (studentId && this.studentsCache.has(studentId)) {
                    return this.studentsCache.get(studentId);
                }
        
                // Does not exist in cache, 
                // so fetch it using whatever HTTP client you're using in the project.
                // Typically a GET request would be done using the input as query parameter (might need sanitization).
                const { data: student } = await axios({
                    method: "get",
                    baseUrl: "https://api.example.com", // Generally comes from some config object elsewhere or an .env file
                    url: `/student/${studentId}`, // Or construct a new URL() and set its params
                    headers: {} // Whatever headers needed (tracking, authorization, MIME...)
                });
        
                // Save to cache. The act of memoization happens here.
                // Notice that we save the INPUT (studentId) and not student.id which is a part of the result itself.
                // Next time, if we encounter a studentId input with the same value, 
                // the API call won't be executed as the function will return early with the previously saved result.
                if (studentId && student) {
                    this.studentsCache.set(studentId, student);
                }
        
                return student;
        
            } catch (error) {
                throw new Error(`[::getStudent::] Error while fetching student ${studentId} - ${error}`);
            }
        }

I did this for every resources that was handled, and the integration ran under 1 hour.
The rest of the execution time is attributable to the thousands of resources it still handles through API calls and we're very rate-limited on the both ends, but a 3 hours gain with such a simple technique is a huge win in my opinion.

Of course, the operation performed can be whatever it needs to be, I took the example of an HTTP call but it can be an expensive math calculation, a huge JSON (de)serialization, anything, it doesn't matter, the idea is the same.

Maybe you already knew this technique or didn't know it had a name, but I figured I'd share it because you would not believe the number of situations I've seen where the same exact operation were executed over and over again.

Anyway, happy coding and have fun.

#development #performance