A little over six months ago the Evernote web team started building a brand new web client. Growing technical debt, difficulties with our current architecture, and an aging GWT stack all motivated us to rebuild. We wanted to use better tools like React, Redux, Webpack, and Babel to enhance our workflow, attract more engineers to our team, and significantly increase our velocity. We started out just thinking we’d rebuild our application on top of Redux, with some RxJS and other ideas sprinkled in. What we ended up with? Well, it’s amazing.
We’re using the ideas of micro-service architectures to bring more flexibility, performance, and maintainability to web application development. I’m not going to go into a ton of detail about exactly how we’re doing this here, but the basic idea is that we’re treating “workers” or processes like VMs and writing a framework that would handle communication, scaling, and distribution of tasks across our workers. We haven’t yet named the abstract architecture, but here at Evernote, we’ve been calling the new web infrastructure that we’re building on top of this framework the “Ion” project.
As often happens, we kind of stumbled into the core breakthrough that gave us the architecture we now have. One of our product managers was looking for a way to run complex experiments that went well beyond simple UI changes. When we started to think about how to accomplish this in an efficient and repeatable way, we realized that we needed a platform that wasn’t so “view-centric.” More often than not, applications are built from the perspective of the presentation layer, and this ends up producing an application architecture that is a slave to the UI. When your UI needs to change, so must your models and controllers. Or, in the context of Redux/Flux, so must your stores and actions.
To make this kind of experimentation possible, we developed an architecture that treated the presentation layer like any other component—it’s a consumer of data, and a producer of events—nothing more. Our presentation layer can then be thought of as an independent application that uses a React/Redux architecture. But our Redux store manages only the part of the application state that relates to the presentation layer at that moment in time. A user’s account data is maintained across a variety of DataSources. Each instance has its own reducer which allows it to maintain its own state and broadcast relevant change events. Other computational tasks like searching, sorting and sync are handled and maintained by other micro-components. All of the components are connected by a unified message broker—one that makes extensive use of the PubSub pattern. You might ask why we’re not just using a simpler approach—maybe an Observable pattern, or even just a standard Redux app. The answer is that in addition to being the next Evernote web client, Ion is also Evernote’s first step towards creating a core client library.
One of Evernote’s many benefits has for years been our presence on a variety of platforms. Historically, for the many talented engineers at Evernote, this has meant writing the core of our applications at least five times, for at least five different platforms. Thankfully, the proliferation of web technologies, web workers, and web views in native platforms means that this approach is no longer a requirement. Instead, we’re working to build a core client library that contains all the business logic necessary to maintain the state of a user’s account on the client side. By using the PubSub-style broker I’ve described above as a “bridge” (along with a bit of native code on the platform in question), we can power all five of our applications with less work and more velocity.
With this PubSub broker in place, whenever a new message is published about a particular topic, it is rebroadcast to all subscribed micro-components in our application. The PubSub pattern allows us to broadcast these messages across processes or threads. This means that interested parties can subscribe to the topics they care about, while others can just ignore them. For example, our client-side search component will be a micro-component that runs in a separate thread, process or worker (depending upon the platform in use). This component will be completely independent—maintaining a simple keyword index for various entities, and using our RPCs to connect to our cloud-based search service. When a user enters search keywords into our UI, a
FIND_NOTES message is published to the
Search topic. The search micro-component—having previously subscribed to the
Search topic—will receive this message and act upon it. If a network connection is available, it will hand the task off to our search service, if not—or if the search service doesn’t respond quickly enough—it will use its own index to generate a basic set of search results. Either way, once results are available, it will publish a
RESULTS_AVAILABLE message back to the
Search topic. The UI consumes this message which leads to the search results being displayed to the user. The key benefit here is that the UI needn’t have any knowledge of how these results are being generated. It only knows that they are. Furthermore, in this example, the events that lead to search results appearing in the UI are asynchronous. We don’t need to write complex logic to control the order in which processing occurs. Things happen when they happen. New messages are then generated (or not) and other components act on those messages (or not). Round-and-round we go. For the few cases where synchronous responses are required, we’re exploring a number of possibilities: Developing a waitFor mechanism much like the one used in Facebook’s Flux dispatcher, or using a new idea we’re calling “connectors”—which are essentially micro-services that function a bit like pipes—to manage the flow of data.
But there’s one more benefit that’s really got me and other members of the engineering leadership here at Evernote excited (you may have already guessed it): Cloud architectures that utilize micro-services have allowed development teams to work in a highly independent, decoupled way without having to concern themselves with existing behavior or functionality, or what other teams are doing. Our micro-services like approach to client-side application development has given us many of the same workflow benefits. Once we had the biggest pieces of the puzzle in place (communication, service management, etc.) we realized that we could build new Ion micro-components without fear of causing regressions within existing functionality or behavior. Eventually, I see us being able to self-organize in ways that haven’t been historically feasible, like organizing our client-side development teams around specific bits of functionality and behavior.
For example, we’re working on a new component that lazily resorts a user’s notes in the background. In previous versions of the Evernote web app, note sorting relied on the Evernote service, and the experience could be easily affected by network latency. Now, notes are continuously sorted across a variety of different attributes. When a user interacts with the sorting UI, a message will be sent to this new component. The component will first request a re-sort from the service, but if the request takes more than 100ms, the client-side component will take over and provide the results—leading to a far snappier experience for our users.
In the future, we’ll build a micro component like the
Sorter with a small team of dedicated engineers. They’ll be able to develop the new component in near-isolation and then iterate on that component in our actual production environment—releasing new code when they have something new to push out. After all, the component can consume messages from an existing ‘topic,’ and the result of its work is harmless at worst—it publishes a new message to the broker.
Thinking about client-side development in terms of micro-components also makes our codebase less fragile and easier to work on. Suppose that a year after the
Sorter component is deemed “complete” we add a new attribute—let’s call it
Note type. Suppose we also want notes to be sortable based on the value of
omega. Even if the engineers who originally built the
Sorter are no longer available, other Evernote engineers should be able to quickly get up to speed and confidently make the necessary changes to the
Sorter micro-component because micro-components are small by nature. There’s less code to read and understand, and even if you do make a mistake, the reach and impact of any regression is limited to the scope of the
Sorter component. Already, I’ve seen my engineers excited about being able to work on our client without the need to maintain a mental model of the entire system at all times. And this should only improve as we refine the underlying architecture.
TL;DR: we’re never going back. And this shouldn’t be surprising when you consider that every major engineering department that has made the switch to micro-service architectures would likely laugh at the idea of moving back to a monolith.
I’m looking forward to sharing more about our application architecture once we release a beta version of Ion to the public early next year. If you want to know more sooner than that, you’ll just have to come work here. We’re always looking for experts software engineers with experience building modular, scalable web applications. Feel like that’s you? Reach out and you can come help us define what comes next.