Edge Native Applications: How to Improve Your Design
Web application architecture has evolved substantially over time, but today it is based on the idea of running code on central, directly controlled platforms. Now, it's time to rethink our design principles once again, to let our applications loose on the edge of the network.
In 2012, one of Heroku's founders created the 12 factor app, a sort of manifesto for the platform-as-a-service era, with the intent of promoting the principles that would make it easy to run apps on Heroku, or indeed any other PAAS (e.g. Google App Engine or the recently launched App Platform from DigitalOcean).
For me, the most important of these is the idea of having stateless processes, which can run multiple instances concurrently, which are tolerant of being shut down arbitrarily, and which connect to other applications that they depend on via standard network protocols.
Turns out, those principles also prepared us pretty well for the serverless world, where the point of abstraction shifted again from being process based to being function based.
Functions as a service changes the playing field a little more: we have to deal with per-invocation (in web app terms, "per request") limits on runtime, CPU time, and memory, and often no access to a filesystem.
Going from managing physical servers, operating systems, hypervisors, container engines, load balancers, etc. to just provisioning functions is amazingly empowering.
Centralization is still holding us back
However, even though you're now deploying just function code, most cloud platforms will make you choose where you want to run it. And the same goes for any attached data stores, message queues, or whatever other virtual infrastructure you need to operate your application. To operate at global scale, you need to think carefully about these choices, figure out replication, conflict resolution or eventual consistency strategies, set up a CDN or other kind of regional caching, and so on.
But if you think about it, having to explicitly and intentionally deploy code in each place is expensive, tedious, and hard to change quickly.
Each of these has separate benefits and drawbacks. As we move further away from the client device, we get a better, more complete view of the global state of the system, but it takes longer to process each transaction.
For example, core infrastructure might be the cheapest place to store large datasets, while the client is the cheapest place to do processing (it's free!), but on the client, we only get access to one user's view of the system. At the edge, we might have access to shared state, but probably not a consistent view of the entire global state.
So in summary, where we ideally put logic in the pipeline is a function of things like
The number of users that are affected or will benefit from the operation;
Whether we need access to state and what kind of state — and whether that state is global or local, and whether it needs to be up to date or complete;
What kind of CPU or memory resources we need to complete the task; and
Any regulatory, privacy, or security considerations.
As well as being faster, one major benefit of moving away from the core is that you don't have to provision anything — the infrastructure that you need is provisioned on demand, wherever it is needed.
Making edge computing part of your architecture
Many of the ideas that shake out of this kind of thinking don't require much of a departure from the way we already think about web applications today.
If you have a publishing-related app with a CMS, why not do your page templating at the edge? Requests from the edge to your core are then API calls to your CMS database.
Or, say you have a single-page app powered by React or Vue. You know about the perf benefits of server-side rendering, but at the edge you could perform a server-side render on demand, in the context of the current user.
Companies offering a public API often have an API gateway that is responsible for authentication, rate limiting, request validation, batching/unbatching, caching, and routing, and this already sits in front of your API backends. Move that to the edge, too!
IoT companies operating fleets of connected devices: your widgets are often low powered and hard to update over the air, so you want to have them emit telemetry that you can monitor. At the edge, you can be receiving vast amounts of data, filtering, and aggregating it, and re-emitting to your core data analysis tools only the information you need.
Becoming edge native by thinking decentralized
So far so good, but to go further, we need to reconsider some pretty fundamental principles of application design. Data stores that are widely distributed are susceptible to partition (losing connectivity between nodes), and the more distantly they are distributed, the higher the synchronization latency and probability of disconnects. CAP tells us that if a data store needs to be partition tolerant, it cannot also be both available and consistent:
This problem tends to encourage applications to hoard their data and logic centrally, to avoid the need for partition-tolerant data stores for as long as possible. Other solutions, such as Conflict-free Replicated Data Types can help but are complex to think about and deploy in production.
But as you explore the benefits of edge computing, it quickly becomes clear that a single, complete and comprehensive view of a "global state" is actually often unnecessary, and by engineering this out of your solution, your application becomes much more portable to an edge platform.
This is particularly interesting for use cases where the writes are updating data that is typically needed to be up to date within the same geographic area where the writes are happening. Dating apps like Tinder, or ride-sharing apps like Uber, are good examples of use cases where the data can be sharded in this way across the world, and still enjoy (but not rely upon) access to a view of the global state.
In some use cases, we can imagine doing away with the core infrastructure entirely.
Consider apps that store and operate on personal datasets, for example Gmail, Pinterest, and Dropbox. These would benefit from locating the primary data store for each individual user close to where that user is physically located. And with edge cloud platforms, maybe you shouldn't need to spin up separate storage instances in multiple cloud locations, instead just allow the edge platform to manage and move the data to where it is used.
Finally, think about scenarios in which one edge location might want to directly communicate with another as part of a specific request or transaction, maybe by relaying chat messages between users who happen to be connected to different edge nodes, or streaming game updates for users in a multiplayer game session.
Two new design principles for edge-native apps
As you go about designing the architecture of your next project, how can you be “edge native?” I suggest we can add two more design factors relating to how we manage state:
Remote state: An edge-native application assumes that persistent state is controlled remotely, but cached locally. It updates local representations of remote state and makes use of mechanisms in the platform that reconcile updates to that state asynchronously.
No single view: An edge-native application does not need to have a complete and up-to-date understanding of the entire world in order to operate correctly.
Thinking in terms of these design principles will help to drive our application development towards an edge-compatible future. Fastly's Compute@Edge platform is evolving to support more of the features you need to build applications at the edge, and we'd love to know what kind of state management features you need.