In this series we are exploring patterns for web API architecture. In order to build our case, we must first take a detour to explore common patterns of code execution whenever you call a remote API endpoint.
When code is executed, it passes through a series of layers, each of which takes some amount of time, until each layer has run to completion and returned, and a value is ultimately presented to the user.
Normally, each of these steps takes a consistent amount of time, unless one of these layers happens to be i/o bound — it is hitting network, disk, or a database.
Regardless of your application architecture — 1, 2, 3, or 10 layers, all “happy path” execution basically looks like a “V” shape. A call stack is built, probably culminating in a data access request from a database (or caching layer), and that data is returned, massaged, and delivered to the client via an HTTP response.
We call this the V shaped execution path. For small web apis, modeling your execution this way works pretty well. As your application begins to scale, however, the V becomes harder to maintain for all but the most trivial of endpoints.
At a certain scale your execution looks more like this:
As an app scales, write operations to relational databases will soon become your bottleneck. Though it’s also possible that read operations can cause slowdowns, generally you can just “put a cache in front of it” and speed things up.
It should be intuitive that relational write operations are a bottleneck — the ACID guarantees provided by relational storage systems like Mysql and Postgres are costly, and as more and more users write to the same table or tables, those systems will slow. Services like AWS Aurora have raised the bar of performance for relational systems, but limits still exist. For our company, our relational database service, powered by AWS Aurora, is one of the most expensive components in our AWS bill each year, driven primarily by Database Storage and IOs.
Ultimately, web application performance starts to resemble a “U”, not a “V”, when your app waits on the results of a database operation. Even if you have multithreaded or non-blocking application servers like nodejs, your application code will still be waiting for a response from the database before providing the user with a response to his POST, PUT or DELETE operation.
At a certain point it becomes clear that, for consistent, reliable, and resilient endpoints, any state mutation requires a queue and an asynchronous confirmation. It is the job of every endpoint to get data back to the caller as soon as possible. In the event of a GET operation, once data is retrieved, that operation is concluded. In the event of a POST, PUT or DELETE operation, that endpoint should again respond to the caller as quickly as possible with a promise of an additional, later callback, likely over a websocket, with the results of that state mutation.
Indeed, this is the exact architecture of gRPC, Google’s remote procedure call framework. Clients make calls and the server responds with 1 or many messages, until the channel is closed. While our API does not (yet) use gRPC, the basic need for a communication loop is at the core of any modern, high performance web API.
Below you can see an example of what a basic single-response operation might look like:
As part of the response to the initial request, we additionally fire off a new asynchronous process that itself may invoke more business logic and data layer operations, with the results ultimately delivered via an asynchronous protocol like websockets.
We’ve outlined what an execution path might look like, but we’ve assumed a shared understanding of the term “execution path”. What happens when things go wrong? We explore this in more depth in our next article on Railway Oriented Programming.
More in this series
- A visual history of web API design
- Patterns of web API execution flows ← you are here
- Railway Oriented Programming
- Clean API Architecture
- Endpoint Responsibility Checklist
- Code example: Saving a favorite
Other series you might like
Android Activity Lifecycle considered harmful (2021)
Android process death, unexplainable NullPointerExceptions, and the MVVM lifecycle you need right now
Kotlin in Xcode? Swift in Android Studio? (2020)
A series on using Clean + MVVM for consistent architecture on iOS & Android