Nothing ever work... mostly #
I often feel I want to rant about how "nothing ever works" in tech.
But today is one of those rare days when I need to shout out to those people in tech who make my life so much easier: the creators of Scala and especially its great ecosystem, in particular ZIO, caliban and http4s.
The challenge #
Recently I've been working on a web application for a client.
This application is a classical analytics tool that displays charts and tables.
What makes it stand out a bit from similar tools is the number of ways in which the user can explore the data, including selecting and combing various filters dynamically, applying grouping, time-ranges, drilling down, etc.
Furthermore, users can only see specific data depending on their roles, so there is no way the application can allow the user access to the underlying data directly (such as tools like tableau or looker do) which would makes things much simpler.
In consequence, there is a backend system with a graphql API that allows the frontend to request the different kinds of data (called "metrics") that the frontend needs and in which shape it needs them in.
For better or worse, the backend then takes such a request and smartly generates SQL queries which are executed by a Postgres database.
The SQL queries that are generated depend on the request. But one thing is important: for performance reasons one request will contain multiple metrics.
And because similar metrics can be resolved by a single SQL query (e.g. because they use the same table), they will be grouped together in the SQL to improve performance and reduce database load.
Within the backend (but transparent to the frontend) the structure looks something like that:
[metric1, metric2, metric4] -> sql1
[metric3, metric6] -> sql2
[metric5, metric7, metric8] -> sql3
In other words, multiple SQL queries will be executed for a single graphql request.
Experienced developers can immediately tell what this means: if there are 10 SQL queries generated and 9 of them resolve really fast but the last query is slow, then this one slow query will delay everything since it has to be completed before the graphql request can be responded to by the backend.
This leads to a bad UX where the user waits for all their results for a long time, even though they could already see 90% of their requested numbers while waiting for the last 10%.
This problem had to be tackled.
Graphql subscriptions to the rescue #
There are certainly different ways to deal with that problem, including scaling up the database or even changing database technology completely.
However, if it were just possible to return the already finished partial results to the frontend, that would already improve the UX drastically with little effort.
Luckily I'm not the first one to deal with this kind of problem. Graphql has a solution builtin: subscriptions.
It allows the backend to use websockets for asynchronous 2-way communication between backend and frontend. And that also means the backend can send data to the frontend in a streaming way. Just what I needed.
Since I had never used websockets before, I was wondering how difficult it would be to get the backend to support it.
To do so, three parts where necessary:
1.) I had to rewrite the backend code that was creating and executing the SQL and then turning it into a proper response for the frontend.
This code was already written with ZIO in an async way, but it just awaited all SQL queries to finish before processing the results.
This had to be turned into a streaming approach using ZIO Streams so that already finished SQL queries would be processed and returned to the frontend immediately without waiting for the other SQL queries.
2.) The graphql API generated by caliban had to be extended to support this kind of subscription. This essentially meant adapting the existing query and turning it into a streaming subscription by using the logic from step 1.).
3.) The webserver configuration had to be changed to support websockets for the new graphql subscription and be made work together with the graphql library.
Of course existing features for regular http-requests such as authentication, logging, metrics etc. should also work for websockets.
The happy surprise #
When it comes to dealing with a new technology that is rather niche while having to get multiple libraries to talk to each other and work together, I'm usually quite skeptical. Those tasks often become a time sink.
That's why I was very positively surprised when I managed to finish all 3 steps in just half a day. And that included having to downgrade the version of http4s to the latest stable version (instead of the previously the "bleeding edge" milestone releases).
This forced a bigger refactoring, but it allowed me to use an integration between caliban and http4s so that I only had to write a few lines of code to get the websockets to work. Thanks to the caliban folks for providing that one!
I then spent the rest of the day trying to get our already setup graphiql to work with new websockets API... but that's a different story.
For me, two things are really remarkable here.
First, Scala's powerful type-system is often disregarded as academic and too complex with little practical benefit.
But here it shows it's power, because it enables such a great ecosystem in which libraries that know nothing of each other can be made to work together with just one line of imports and sometimes a few extra lines for configuration or a small separate dependency (which can be provided by a totally separate party).
This is often missed by people: the type of a programming language has a huge impact on the quality of its ecosystem.
Second, I find it amazing that I can rewrite a crucial piece of the application logic to work in a lazily-streaming way, downgrade the http library, add a new project dependency and integrate it into the code, glue things together, then fix the millions of compile errors and then... it just works and runs all smoothly.
There was no manual or automated testing or other kind of trial&error during the whole process - just fixing compile errors and then on the first run it all worked exactly as expected and the data was streamed lazily just like I planned (which I could confirm after being able to fix graphiql).
And while it's certainly not hard to set up some graphql subscription POC in almost any language / ecosystem from scratch, it is a whole different story to do that within an existing application and having to apply quite the refactoring for it while not breaking anything in the process.
So for me, the Scala way of doing things is not only a huge time saver and makes me super productive.
It also just feels much better than running and fixing tests one by one or even running the application and testing it manually, just to find that something breaks at runtime.
Even just the pleasure of this way of working makes learning some harder concepts of the Scala language more than worth it.