The future of e-commerce requires personalized content for each individual customer. With over 20 million active users, Zalando is facing the challenge of making its frontends more scalable in order to achieve this goal. We brought a solution from the backend to the frontend: microservices. In addition to creating Project Mosaic (https://www.mosaic9.org)—our solution for modular frontends—and using a toolbox of modern web technologies like React and Webpack, we built an architecture that can scale to virtually any size and even support targets other than the browser.
4. EUROPE’S LEADING ONLINE FASHION PLATFORM
15 countries
21+ million active customers
~3.6 billion € revenue 2016
200+ million visits per month
13.000+ employees in Europe
1.600 tech employees
Visit us: tech.zalando.com
13. 13
Conway’s Law
“organizations which design systems
...are constrained to produce
designs which are copies of the
communication structures of these
organizations”
22. Translation Service
Team Pathfinder
IAM API
Team GreendaleFRAGMENT
Your Team API
Your Team
From Tailor
HTML Render
AJAX APIs
Internal API Client
From Skipper
Cart Service
Team COAST
27. 27
SKIPPER
Forwards requests to different
endpoints based on request properties:
Host, Path, Method
Cookies, etc.
Streams content from the endpoints
Runtime updates of routing table
github.com/zalando/skipper
28. 28
Tailor is a layout service that
uses streams to compose a web
page from fragment services.
Loads content of all fragments
from the template in parallel.
Offers nice error handling and
fallback features.
github.com/zalando/tailor
30. HEADER
CART
TAILOR
layout service
CART FRAGMENT
Team COAST
HEADER FRAGMENT
Team Navigation
QUILT
template management API
CART TEMPLATE
TRACKING
TRACKING
FRAGMENT
Team TRCKNG
https://cart.coast.zalan.do
https://eb-fragment.trckng.zalan.do
https://header-fragment-release.navigation.zalan.do
From Skipper
https://zalando.de/cart
31. 31
FRAGMENT JAVASCRIPT
FRAGMENT SKIPPER
router
TAILOR
layout service
CLIENT
STATIC HTML
<button>click me</button>
LINK HEADERS
<script.js>; rel="fragment-script"
<style.css>; rel="stylesheet"
AMD MODULE JAVASCRIPT
define([], () => element => {
element.onclick = () =>
alert('Hello, World!')
})
32. 32
FRAGMENT JAVASCRIPT
FRAGMENT SKIPPER
router
TAILOR
layout service
CLIENT
STATIC HTML
<button>click me</button>
LINK HEADERS
<script.js>; rel="fragment-script"
<style.css>; rel="stylesheet"
AMD MODULE JAVASCRIPT
define([], () => element => {
element.onclick = () =>
alert('Hello, World!')
})
41. 41
FRAGMENT ARCHITECTURE
FRAGMENT
let x = parseRequest(req)
let data = await fetch(x)
let html = template(data)
res.write(html)
BETTER
HTML templates
External content data
Dynamic responses
DOESN’T SCALE
Still inconsistent
JavaScript is 2nd class
56. 56
TESSELLATE: TRANSFORM JSON INTO JAVASCRIPT
Build portable UMD bundles
Run webpack in memory github.com/mfellner/webpack-sandboxed
Export a root component, export a render function for Tailor
Interface for injected property values
Support JSON Pointers in props { “$ref”: “#/attrs/value” }
Inline props for rehydration
Include external component libraries from npm
{ type: “[node-module-name].[export-name]” }
57. 57
TESSELLATE: RENDER JAVASCRIPT INTO HTML
.HTML.JS
TESSELLATE
fragment
CDN
static server
1. fetch webpack bundles
2. load external data
3. execute JavaScript code
4. render static HTML
renderToString()
58. 58
TESSELLATE: RENDER JAVASCRIPT INTO HTML
Fetch the precompiled bundle …
const code = await fetchBundle()
const { Root } = vm.runInNewContext(code)
const props = await fetchContent()
const element = React.createElement(Root, props)
ReactDOMServer.renderToString(element)
59. 59
TESSELLATE: RENDER JAVASCRIPT INTO HTML
Run the code in the Node vm …
const code = await fetchBundle()
const { Root } = vm.runInNewContext(code)
const props = await fetchContent()
const element = React.createElement(Root, props)
ReactDOMServer.renderToString(element)
60. 60
TESSELLATE: RENDER JAVASCRIPT INTO HTML
Fetch any external content …
const code = await fetchBundle()
const { Root } = vm.runInNewContext(code)
const props = await fetchContent()
const element = React.createElement(Root, props)
ReactDOMServer.renderToString(element)
61. 61
TESSELLATE: RENDER JAVASCRIPT INTO HTML
Render to HTML!
const code = await fetchBundle()
const { Root } = vm.runInNewContext(code)
const props = await fetchContent()
const element = React.createElement(Root, props)
ReactDOMServer.renderToString(element)
62. 62
TESSELLATE: RENDER JAVASCRIPT INTO HTML
Download the code
Precompiled bundle and any dependencies.
Fetch external content
To be injected as properties into the root component.
Send to Tailor
Rendered HTML and links to JavaScript (according to the Fragment API).
Total length: 40 minutes (35m talk, 5m Q&A). License: https://creativecommons.org/licenses/by-nc-sa/3.0
Hello everybody, welcome to our talk, The Recipe for Scalable Frontends.
My name is Dan Persa, I’m an Engineering Lead in the Fashion Store department at Zalando and one of my challenges now, is to lead the team which is building a next generation content management system. I’m also one of the developers who contributed to the Project Mosaic, the one we’ll be talking about today.
Hi, I’m Maximilian Fellner and I’ve been working on web services and applications at Zalando for several years. Recently I developed a new open source project that aims to make frontends more scalable and you’ll learn about that today as well.
Zalando is Europe’s leading online fashion platform.
Here are some numbers to show why our recipe for scaling works. It’s because we’re dealing with scaling every day!
Our company is present in 15 countries, we have more than 21mil active customers and last year we had a 3.6 billion E in revenue.
We have a great team of 1600 talented people in tech.
And here is how our Fashion Store looks like!
Now, that you have an idea about the scale at which our company operates, let’s get to our “Recipe for Scalable Frontends”
Every recipe has two important things:
The ingredients
The instructions
We’ll soon discover ingredients related to scalability and ingredients related to frontends, we’ll get our hands dirty and get into the technical instructions as well.
Let’s take a look at the first 2 essential ingredients we will use for our recipe:
Scaling the tech team
Scaling the architecture
Without these two ingredients, scaling the frontend might not even make sense, as the system operates at a such low capacity, that scaling is not a problem worth tackling.
Let’s dive deeper and see why each of these two is important.
First we want to be able to scale the tech team. What does this mean?
It means attracting new, talented people, to grow the team. The reason you want to scale the team in general, and specifically in our case, the frontend team, is because there are plenty of new features to be added to existing products or new products to be built.
Of course, this can’t be achieved if there are more frontenders leaving the company, than coming, so another requirement is to keep the existing team happy. Putting somebody to spend 4 hours every week to deploy a monolith, doesn’t make anybody happy!! Having the frontend developers waiting for a monolith to compile, let’s say, its Java classes, is also not appealing!
We want diverse people who are able to see the same problem from different perspectives. The more ideas you have, the more likely it is to find the best one. Diversity leads to innovation.
We also want to actively encourage innovation. Innovation is keeping the company on top of the competition and it can be encouraged by organizing hack weeks and giving the teams time to build prototypes and experiment.
A happy team is an innovative team.
For a happy, innovative team it’s easy to talk about the projects they are working on (like me and Max today) and attract other talented people.
As you can see, all four points are interconnected!
Let’s look and see how, applying these principles helped our company scale its tech team.
Here is how our tech department evolved over time… as we can see, we started in 2008, with PHP and MySQL, Magento actually :)
One of my colleagues created this chart two years ago, to demonstrate the growth of our apps, in comparison to our tech team…
What we can see here is more or less linear growth for both the number of employees and apps.
Starting 2015, it had to be slightly adjusted.
As you can see, the number of apps exploded! We have more than 500. And since then, we started to have plenty of open source repos in GitHub.
The number of people in tech is more than double compared to two years ago.
So, looking at this numbers, you might ask yourselves - how can this be possible?
The answer is simple: Two years ago, we introduced a new concept, Radical Agility.
We, at Zalando, understand Radical Agility as a way of running a business based on purpose, autonomy and mastery. To describe this in one sentence would be: Trust instead of control!
The final goals of Radical Agility are being represented on the edge of the slide: Increase innovation, increase productivity.
What’s important for today’s talk, is that Radical Agility changed the communication structure of our tech department. We started to be organized into small, autonomous engineering teams which are trusted and accept responsibility.
So here we have our first ingredient: Scaling the team! Hire diverse people, give them the freedom to self organize, give them a clear, motivating purpose, and trust that they’ll deliver great features.
Reorganizing the team has consequences on the the architecture side as well:
Let me introduce Conway’s Law: “organizations produce designs which are copies of their communication structures”.
So as soon as a company reorganizes its team, the architecture also has to be changed.
Which brings us to the next ingredient:
Architecture scalability:
What do we want to achieve here?
We want many teams, working in parallel, at different features, without interfering with each other. Team Autonomy.
We want them to deploy independently - we want to allow them to do continuous delivery, in case they want this
We want programming language diversity - we want to use the right tool for the job. In our case, we used to be a java based company, now we’re putting more and more languages to production.
A/B Testing: Adding and removing features really fast, drives the products forward. Experimenting with features is a great way to innovate and improve, on the product side.
But wait a second, these are some of the promises microservices make on the backend.
So the backend part is somehow easy, we fulfill these promises by creating backend APIs owned by teams.
The questions is: do microservices solve the frontend problems we have?? Do these promises apply here as well?
Let’s take a look at the traditional way of solving the frontend problem.
The traditional solution, without microservices, would be to have a frontend monolith.
As backend teams own APIs now, we can call those APIs from a more lightweight frontend monolith.
The webapp gets contributions from multiple frontend teams.
Let’s look again at some of the things we’re trying to achieve:
Are the frontend teams working autonomously?
No, they’re not. They still depend on each other, as they’re contributing to the same lightweight frontend monolith.
Can they use and mix different tech stacks?
Not really, as somebody has to choose a framework for the common frontend.
Can they deploy independently, whenever they want?
No, they are still bound to a release cycle.
It seems that avoiding frontend microservices and solving the challenges we have, is not an option. We somehow have to make it work!
Here is the Mosaic Project! Microservices for the frontend!
Mosaic is a set of services and libraries, most of them open sourced, together with a specification, that defines how its components interact with each other.
The goal, is to support a microservice style architecture for large scale websites.
While decomposing the backend into microservices is a widely adopted approach to achieve flexibility in development and operation, most frontend solutions are still running as a monolithic application.
Mosaic addresses this issue by using Fragments, services that render html that is composed together, at runtime, according to template definitions.
The fragments are frontend microservices owned by different teams. Teams should be able to develop, deploy and operate these services independently of each other.
What’s happening inside of a fragment? :) We can render HTML, fetch data, and we can respond to AJAX requests. Ideally there is no business logic within these fragments.
None of these actions are mandatory, the ones implementing the fragment can choose.
As a result, Fragments can be iterated on very rapidly, be more flexible in technology choices, and can better benefit from the extreme development pace of today's frontend technologies.
As I said, in this new setup, each team owns its own fragments, they are able to choose the technology for them and can deploy them independently. Each fragment is able to do calls to different APIs. And each fragment renders a part of a final page. How does the final page get assembled from the html rendered by each of the fragments?
On top of the fragments we have another service called the Layout Service. The Layout Service puts together the content rendered by fragments. It uses templates to know how to assemble them. We’ll talk about templates soon.
The assembled content is streamed to the client, through a router. The router is necessary to handle API calls, mobile app calls or help in the migration from a legacy system.
Let’s get from this microservices for the frontend idea, to the Mosaic implementation. We have names for both the router and the layout service. Skipper is our router and Tailor our layout service.
Skipper, is a piece of software we built in house, developed as an open source project. It is the entry point for every request from the browser (excluding static resources)
It acts like a reverse proxy, and will delegate the request, based on some internal routes, to one of our new microservices, or to a legacy system, like Jimmy there, in case a migration is happening
It has other functions, like security features as well. For more details check out the github repository, at github.com/zalando/skipper
Tailor is the Layout Service implementation, another open source project that we built. It uses streams to compose a web page from fragment services.
It loads the content of all fragments from the template in parallel, offers nice error handling and fallback features.
Here is how a Tailor template looks like. As you can see, most of it is html, with some special tag: Fragment. This tag acts as a placeholder, where Tailor will insert the content it gets from each specified fragment.
One of the fragments has to be marked marked as primary. This is the fragment which will give the http status of the final page.
You might have noticed that this concept is similar to Facebook’s BigPipe. The difference is, that with the Big Pipe, things get assembled on the client side. For SEO and user experience reasons we want most our content to be streamed in order. We can achieve a similar behavior as with the Big Pipe by using the async attribute.
Let’s look at an example:
We have a request for the cart page from skipper, the router. Skipper knows that the “/cart” url should be handled by Tailor, and forwards the request to it.
Tailor needs the Template for the “/cart” url and in case it doesn’t have it cached, it asks Quilt, a RESTful API for it.
Tailor gets the template, looks at it, and sees that it has to call the header fragment, the cart fragment and the tracking fragment. It does all of these calls in parallel, puts everything together according to the template and returns the response to skipper. The status code gets set by the cart fragment, as this is the primary fragment.
A fragment may provide some JavaScript and CSS as well. Besides the static HTML, Tailor will look for additional script and stylesheet assets inside the “Link” HTTP header of the fragment response. And will inject the corresponding links in the head section of the final page.
The JavaScript must be defined as an AMD (Asynchronous module definition) module that exports a single function. When finally executed on the client, this function receives the DOM element of the fragment as an argument.
Because each fragment is an isolated standalone application inside the browser, fragments need a way to communicate with each other. For example, one fragment might add and article to the shopping cart. As a result of this action, another fragment wants to update the list of shopping cart items.
Although there is no solution built into Mosaic itself, it is easy to add a shared communication bus that fragments can publish and subscribe to. On zalando.de we are using, the “happened” library (github.com/grassator/happened).
Communication Between Fragments
To facilitate communication between Fragments Mosaic suggests to use an event bus. In the Fashion Store the following approach is used. As part of the base assets an event bus based on happened is provided. Fragments that need to communicate are encouraged to register or listen to events. To that end, each fragment repository contains event definitions such that other fragment owners can figure out which events a fragment provides and listens to.
Communication Between Server Side Mosaic Components
It is strongly recommended to ensure that fragments handle requests only from Tailor or Skipper: oauth2 and firewalling.
https://pages.github.bus.zalan.do/pathfinder/mosaic-documentation/Fragments.html?highlight=event#communication-between-fragments
Here is how our Zalando cart page looks like, after it gets assembled by Tailor.
The final page is assembled from 3 fragments: the header fragment, the cart fragment and another fragment that is not visible, the tracking fragment.
Each of these fragments are managed by different teams.
So here is our second ingredient: Scaling the architecture!
Come up with an architecture which allows team autonomy, independent deployment, encourages a mix of tech stacks and experimentation.
Right now, Mosaic is live, we have quite some traffic. Last year on Black Friday we had around 21 thousand requests/s on skipper and we are expecting even more this year soon, as Black Friday is about to come again.
Dan: So far I have showed you how a single large website can be split into smaller fragments. Max, what’s next??
Well, as it turns out, websites are a lot more complex today than they used to be. They contain lots of interactive elements powered by JavaScript and they serve large amounts of dynamic content. Let me show you how we think we’ll manage all that!
Our website, or as we call it, the Fashion Store, is actually a highly dynamic application. Here’s an example how the Fashion Store might be divided into separate fragments. Each fragment can have some sophisticated interactive behaviour and may be talking to multiple backend APIs. Naturally, we update the content on the Fashion Store very frequently with latest styles, trends and product information. The content itself is also dynamic and might change for different users because of personalization.
With this is mind, what is actually the best way to implement a fragment? There are three important things that we want from our fragment:
First, it must provide for our users a modern and interactive user experience similar to a web application. Second, it needs to support the dynamic and frequently updated content that we want to deliver to our customers. And lastly, the fragment has to preserve the consistent look and feel that customers have come to expect everywhere inside the Fashion Store. Given these constraints, let’s consider our implementation options, starting with the most naive approach:
A very simple fragment might only render some static content. Obviously this has many drawbacks. The implementation is too manual and updating the content means changing the whole fragment and redeploying the web service. Also, the user experience is very likely going to be quite inconsistent between different independent teams.
Here is another idea: instead of rendering just static HTML a fragment may react to requests dynamically, fetch some external content and send back a computed response using an HTML template engine. While this is already a lot better than the first approach, many fragments are still going to deliver inconsistent user experiences as implementations differ. The biggest drawback however is that JavaScript is not part of the fragment.
So we have come up with a better concept: fragments should use existing, reusable and shared JavaScript components. Although those components run dynamically as code in the browser, they can also be readily transformed into static HTML on the server. This kind of code that allows server-side rendering is sometimes called isomorphic or universal JavaScript and many popular solutions, for example React or Vue.js, support it.
Now let’s take a closer look at how this idea is supposed to work in general and how it fits together with the Mosaic architecture.
Because the fragment is supposed to use only existing JavaScript components for the content of the website, we start out by describing the components that we want to use with only basic JSON.
If we take React components for example, we can show that they are easily described with JSON. Every component has only three attributes: The type, which can be the name of a DOM element like “div” or “h1”. A set of properties or “props”, for example, the attributes of an HTML tag. And a list of children with more components or simply plain strings.
The next step is for the fragment to take those JSON descriptions and transform them into the JavaScript and HTML that will be sent to the Tailor layout service. During this process the code from common component libraries, for example from modules on npm, can be included.
So how does this code generation work? As we have seen before, React components are actually quite simple. If we know the type, properties and children, we can easily produce a hierarchy of nested function calls of the “createElement” function.
Once the code is generated, we evaluate it to create a React element and then use the “renderToString” function to turn it into static HTML.
The big advantage of this solution is of course that the generated JavaScript is universal, so it can run not only on the server but also on the client. So this really looks like a very promising approach...
In fact, we have already implemented it and released it as an open source project. We’ve called it “Tessellate”. The project comprises two major parts that I’ll talk about in more detail next: the bundler and the renderer.
In Tessellate, the process of transforming JSON into JavaScript code is performed by the bundler in four steps: First the JSON is parsed as an abstract syntax tree. Next, we use that to generate JavaScript code. In the third step, Tessellate uses webpack directly in memory to compile universal bundles from that code. Finally, those bundles are exported to a remote content delivery server for later use.
Let’s inspect an example of the code that Tessellate generates. It’s actually just a single React component!
The component types used in the JSON description are turned into import statements. There can be default imports and named imports. The rest of the JSON description is turned into “createElement” functions with props and children.
However there is more that we can do here. Right now, all the property values are statically encoded directly inside the JSON description. But of course the root component can receive props from an outside source.
So Tessellate has a way to inject external properties into the compiled React components during rendering. The values of those properties are not known ahead of time but they be can be referenced relative to the root props using JSON pointers. In addition to that, we can also bundle some properties during compilation as default values and merge them with the external properties.
A common problem with React components rendered on the server is the so-called “rehydration”. We need to ensure that the same properties are used for server-side and client-side rendering. Tessellate solves this by adding an attribute to the root component that stores all the props as an inlined string. This attribute will be present inside the static HTML allowing us to retrieve the props in the browser!
Finally, the generated JavaScript exports the function that the Tailor API demands. This function is going to be called on the client side inside the browser and it receives the DOM element of the fragment as an argument. This element is actually our root component. Inside the function the inlined props are extracted and the React component tree is mounted on the root element by calling the “render” function.
So to summarize, the Tessellate bundler is a micro service that transforms JSON into JavaScript. The JavaScript is compiled into portable, universal UMD bundles using webpack in memory. The main bundle exports a root component and a “render” function for Tailor.
The root component provides an interface to inject property values. The properties can be referenced using JSON pointers and they’re inlined as a string into HTML for rehydration. We’ll come back to this in a moment.
We’ve also seen how components from external libraries can be referenced in JSON by their respective node module names and names of the exports of those modules.
This was the first part of Tessellate: the bundler.
The other part of Tessellate is the renderer. This is actually the fragment that renders the HTML. When this fragment receives a request, it fetches a precompiled bundle from the CDN where the bundler placed it before. Next, additional data can be loaded from external sources. Then Tessellate evaluates the code and finally renders static HTML.
Here’s what the implementation of that process looks like: first the precompiled bundle is downloaded. At this point, Tessellate can use the available information in the current HTTP request to decide which bundle exactly to choose.
Then the code is evaluated inside the Node JS virtual machine and the exported Root component is extracted.
As we’ve seen before, this root component has an interface for external props and we can load some external data to provide those.
In the final step the root component is instantiated as a React element and injected with the external props. Now the element can be rendered into HTML and sent back to the Tailor layout service.
If it looks like the renderer is a lot simpler than the bundler then that’s because it is! The Tessellate fragment has only three simple jobs: download the precompiled bundle, fetch some additional external content and then render everything to HTML and send it to Tailor.
This is how the complete picture looks like: we have the Tessellate bundler micro service that takes an abstract description of React components written in plain JSON. It compiles the description into universal JavaScript that can run on both the server and inside the browser.
And then there is the Tessellate fragment which renders the compiled bundles and any additional external data into static HTML according to the API demanded by the Tailor layout service.
This architecture meets all our requirements for the Fashion Store: With JavaScript components we can achieve a modern and interactive user experience. Content can be externalized and updated separately. And because we’re using shared component libraries, the look and feel is going to be consistent everywhere.
In fact, this is was the last ingredient of our recipe: Scaling the content! We use small, reusable and shared JavaScript components to push large amounts of content. The components are consistent between many independent teams and they decouple layout and data for greater scalability.
Dan: So finally, here is our recipe for scalable frontends: We have scaling the team, scaling the architecture and scaling the content as ingredients and Radical Agility, Mosaic and Tessellate as instructions.
You can find more details about what we talked about on our project page, mosaic9.org.
Thanks a lot for watching our presentation. Are there any questions?