About this talk
For a few years now, Netflix has been leveraging Ember to build ambitious applications that help us manage and produce billions of dollars in content. In this talk, we'll share our lessons learned, talk about some of the common problems we've faced, and how we've solved them.
Hello, everyone. Thanks for having us at ReactFest. We're going to talk about React, obviously. So at Netflix, we like React. And we like it so much we wrote a blog post about it. We even hired Erik Bryn-- Clearly, a true React lover. Best known for his work in React Model, a lightweight model library for ReactJS. To help with rewriting all of our apps in React. Thanks, Erik. In all seriousness, we're super excited to be here today to talk to you guys about Ember at Netflix. But before we begin, we'd like to first introduce ourselves. I'm Offir, also known as Offir on GitHub, Twitter, and Slack. I'm a full-time cat dad of two incredibly weird cats. And I also happen to build some Ember add-ons every once in a while. And I'm Lauren, also known as Sugar Pirate or Poteto. In my free time, I like travelling. And I'm Australian, so I have to fly everywhere, obviously. I also like computers a lot. And, like Offir, I've also worked on an add-on or two in my free time. But seriously, so Netflix sponsored EmberConf this year in Portland, and it's no secret that we use Ember pretty heavily. But first, I want to set the stage for how we got here. So, around 20 years ago, we did something pretty crazy. And we sent our customers DVDs in the mail because nobody likes going outside anymore. And we actually still do this today, we still send out DVDs. A decade later, we've shipped more than a billion DVDs. That's a lot of DVDs. And then we did another crazy thing, and we started streaming video on demand. So today, Netflix is the best website on the internet, clearly. We have over 100 million members all over the world, and here we are again doing yet another crazy thing. Rewriting all of our apps in React. No, I'm sorry, I'm just kidding. But seriously, since 2013, with House of Cards, we've been producing more and more original content. And we're actually doing this from start to finish. We're basically becoming a studio, and it hasn't been an easy task managing billions of dollars of money on all of this original content. So that's where our team comes in, the Studio Engineering team where me and Offir work. We build applications across the entire studio workflow, from pitch all the way to post. And today we want to share with you some of the things we do with Ember to help us manage this huge volume of original content that we're producing. So firstly, I want to give a bit of more context about why we even chose Ember in the first place. As of right now, we have about 20 large, non-trivial Ember applications in production at Netflix. Our applications help us solve many different workflows across the industry from legal, to finance, to production, to planning. And they're all written in Ember. As we looked back at these applications, we started to notice a lot of common patterns and pain points across the organisation. From authentication to sharing data models to components, you name it, we have that issue. And many of which I imagine you are probably experiencing as well in your companies. So one thing about Ember that really helped us in our decision to choose it was its convention over configuration philosophy. At Netflix, we have the concept of something called a paved road, and a paved road is really a set of expectations and commitments between our centralised platform teams, as well as our engineering teams. And Ember fits really well with this approach. For example, EmberCLI and its add-on ecosystem have helped us standardise and share our code among all of our teams. And we've been able to focus on just getting stuff done, instead of bike-shedding and rebuilding the wheel and modifying our Webpack configurations, we don't need to do any of that. Some of the add-ons we use most frequently are Ember Metrics, which gives you a single interface to call analytics across different products, Ember Concurrency, which brings much needed sanity back to our UIs. I love this picture, by the way. And obviously, Ember CLI Deploy. So Ember allows us to make use of these standard, very standard community solutions to common problems that almost every application faces. And it helps us be more productive. So now let's talk about our first common pain point, which is authentication. Many of our users workflows span across multiple applications, across all sorts of different, I guess, specialisations, like in my previous, legal, finance, et cetera. So that means when they go to another application, and another application, and then another one, you'll notice a trend here. It's pretty annoying. You have to log in to all of these different applications every single time, it's pretty annoying. And it's also a very bad user experience, in my opinion. So we built this thing called Bolt. Bolt is our lightning deploying system that integrates really tightly with other tools in our organisation. And it lets us deploy apps in under a minute, and it also has really cool features, like really fast rollbacks and cool things like that. But more importantly, it's enabled us to develop this shared authentication layer, which can be very easily integrated with an internally built Ember add-on. Bolt is an implementation of the lightning deploying system that Luke Melia demoed at RailsConf a couple years ago. So it's set up with Amazon S3, and also with Redis. But we do have an Apache layer in front. So Bolt integrates with Meechum, which is our internal Netflix identity service with a third is Apache module. And then Meechum, in turn, integrates with other identity providers like Google, so our employees, contractors, vendors have access in some form to our suite of applications. One thing that's really cool about Meechum is that it provides us a single sign-on experience, so you no longer need to sign into all these different applications, you just sign in once to Google, and then to Meechum, and then now you're just logged in for all of our applications. And it also has really nice features, like multifactor authentication that we can leverage. So, when a user first comes to application, if their browser assertion is unauthenticated, they'll get redirected by Bolt to a Meechum login page. And what this means is that the request will never even hit our application if they are unauthenticated. And with this approach, it's pretty nice actually, because application developers can just focus on the core business logic for the application. We don't even need an off-layer in our apps, we don't even need Devise, for example. And instead, off is taken care of for us by a focus platform team for the entire organisation. So this is really nice. So let's dive into how we integrated this in Ember. Obviously, it's a lot of back-end work. But on the front-end work, it wasn't really that much effort either. So after you authenticate, the user will receive a JSON Web Token, or a JWT, back from Meechum. And this JWT has scopes that will allow the browser to access both the UI and the API for this application. We then wrote a very simple Ember data adapter that would add this token for you as an off-hitter. And also a service in this add-on that would periodically pull Meechum for a new token when the current one expires. When the session does expire, then our service will let the user refresh their token from Meechum really easily. And this means that the user doesn't need to refresh their screen, even if their session expires. As long as they're still signed in with their identity provider, like Google. Instead, they can just hit this button, restore their session, and continue using the application. So in order for this architecture to work with this separate API and UI layer, obviously, the token that you get back from Meechum needs to have the scopes for both of the API and the UI so that you can use the same token to make requests again, to your API, to get data. And that means that our APIs also, in turn, integrate with Meechum to validate this token. And then, after the token is validated, we receive the user metadata and request hitters after going through this Meechum proxy. As you can see over here, the identity hitter, that's what we get back. So even if we don't have Devise, for example, in our Rails API, we get this information in the hitters so the current user is still there. So great, we have authentication in place, but now we actually need to build something, right? Before we continue, we should give you more context about the kind of applications we're building. So before our team came along, this is actually, I'm serious, this is how a Netflix original got made. With lots of Word documents, with Google sheets. I think we even used a fax machine at one point. And everybody loved this feature, which is like the real-time collaboration, it was pretty cool, the two people on the keyboard. That was really good. And basically, the issue is that we have all this data about all these different shows that we're producing. But at the time, before our team came along, we didn't have any tools to deal with this data. So in the next couple of sections, we want to look at some of the approaches we've adopted in Ember to help us deal with all of this data. Now, one common feature for most of our applications is that most of them have filters of some kind. And because we're producing so many shows at such a large scale, we actually need, it's not even a nice to have, it's a must have feature for data filtering and querying so that the user makes sense of all the data on the screen, because if you try to load everything, then it's just too much, you can't make sense of it. And we usually do this with query params. I don't know about you, but it's my favourite thing about Ember, it's the best thing ever. So here's the average Ember route. You have a model hook, and you might fetch some data in your model hook when you've entered the route, or if you change your query param or something like that. And when this premise in the model hook resolves, the route will set up your controller and then render your appropriate template for you. So at Neflix, we use query params very frequently to serialise the state of our UI. But we also use query parameters to query our APIs. The kind of implementation of query params in Ember is a little bit verbose, as you can see. You'll see that you repeat properties all over the place, like in your controller twice. One more in your route, because why not. And it's not always clear where the configuration for the query params-- Like, why is this in the route, and that kind of, like, seems arbitrary. So in one of our applications, we decided to write a little small library to make dealing with query params easier. This started out as just a very simple application-type utility. And we made use the fact that you can actually dynamically generate mixins in Ember at runtime. So long as you have a function that returns a mixin. This is actually pretty powerful. You can then use this dynamically generated mixin inside of your controller, component, or any other Ember object, just like you would any other mixin. And this is the approach we used initially to spike this out. And we generate a route and controller mixin from a single object. And doing it this way, we now have this single source of truth, almost, for query parameters so we don't have to edit it in like five different places. If we want to clean it up. Our initial approach was rather, kind of weird. But we've since cleaned it up, and we've even opened sources, and it looks better than that. So together, with Offir, we've released this add-on called Ember Parachute. If you're wondering why we call it Ember Parachute, it's supposed to be a pun on param-achute, but that was weird. So this add-on makes dealing with query params easier, and it actually allows you to move all of your data-loading logic out of your route and into the controller. Let me repeat that, move all of your data-loading logic out of your route and into your controller. So you heard me right, user controller, routable components are not a thing, not yet, and controls are not dead. All right, so now I'm going to pass you over to Offir to explain more about this add-on. All right, so let's start with a simple example. Here we have a form with a search input that searches GitHub's userbase. The flow for an application would look something like this. When we enter the route, we fire an asynchronous call via the model hook. Because we returned a promise from the model hook, Ember transitions us into a loading substate which, in turn, will render the loading template. For sakes of this example, let's have this be our loading template. Once the model promise gets resolved, we enter the controller, which renders our search input along with the rest of our form. And as we start to type, it change action gets bubbled up to the controller. Since it's tied to a query parameter, the route will fire another asynchronous call via the model hook, and again, enter the loading substate. As you can see here, as I start to type, our form gets completely replaced by the loading state. This leads to a pretty bad user experience. The application feels choppy, and can make common tasks for users very frustrating. This is where Ember Parachute excels. This add-on provides a simple interface to define your query parameters which can be then mixed into any controller you want. Once it's mixed in, we get a lot of really nice features. We're able to access the state of a query parameter, like what is its default value? What is its current value? How has its query parameter changed? We're able to reset the query parameters back to their default values, and we get a couple of hooks that we can tap into. One of which is the query params, the change hook, which allows us to do some sort of task when a query param changes, like fetch some data, make a metrics call, et cetera. In this case, we perform an Ember Concurrency task to fetch a set of users with the given search query. So this is what our new flow looks like now. You may notice that we no longer have a loading substate, nor do we have to rely on the route to fetch our data. This time, when we enter the route, we enter immediately into the controller. Our search input, along with the rest of our form, gets rendered, and as we start to type, a change action will get bubbled up to the controller. Ember Parachute will fire its query params to change event. Which we can use to call our Ember Concurrency task, and fetch a new set of users from GitHub. We now have much more granular control as to how we can show our loading state. In this example, when our Ember Concurrency task is running, we show some skeleton or placeholder UI. This allows us to easily create a much more seamless user experience. Looking more into the future, this is what we believe data loading could look like. A component will receive some sort of state from a high-order instance, such as a controller, or a route, or even another component. Which will ultimately decide which of the component's template to load. This could allow us to handle a loading state, or even an error state, among many more, in a much more granular fashion. In the studio world today, there's a tonne of data that we try to collect to better improve and optimise how we function. We collect everything from where we shoot, when we shoot it, how much it costs us, who is in the cast list, what type of camera they're shooting with, who's in the crew list, and what time do they have to be on set, to knowing how Brad Pitt likes his coffee. When studio engineering first started, we had a single application called Origin Story, that has a monolith of an API with over 100 data models. Origin Story attempts to take in as much data about a series feature, et cetera, and make it available to all its downstream applications. As we started to develop more and more apps that used the same API, it quickly grew out of hand. The first thing we did was pull out all our models out of the applications and create a single add-on shooting across the entire organisation. This allowed each app to either pick and choose which models they wanted to import, or with a little broccoli, import them all via configuration option. So what about testing? Setting up a test environment for over a dozen endpoints, a hundred plus data models for multiple applications seemed almost impossible. Even if it was achievable, it'd be way too difficult to maintain. This is where Ember CLI Mirage comes in. With this amazingly intuitive Ember data ORM, we could easily map our Ember data models to Mirage models. But that means we'd have to map all our Ember data models to Mirage models. The issue with that is it's incredibly tedious, it's prone to failure, and it can easily become outdated without the proper care. So we made an add-on that will do this for us. And a few days later, we baked it into Ember CLI Mirage which Tiago mentioned, which you can all use today, and is enabled by default. All right, that's cool and all, but how do we actually do this? It turns out that Mirage already had the right architecture to support this. Because it already registers all its existing Mirage models at runtime, we were able to easily tap into that logic. All we had to do was figure out how to actually create these models, which, as it turns out, wasn't too difficult either. The first thing we do is find all the defined Ember data models in your application. Luckily, this can be done since Ember CLI uses requirejs under the hood. So this screenshot right here is of travis-ci.org, and if you don't already know this, you can go into the console of any Ember app today and you can type in requirejs.entries in the console, and it will show you all of the define modules that the app uses. Everything from models to components to private add-on utilities. Since all the entry keys are the path of the modules, we can filter over them with a regular expression to get all the model entries. All right, so now that we have all our Ember data models, all we have to do is iterate over each of them and map them to a Mirage model. We look at all the belongsTo and hasMany relationships and port them over to the Mirage implementation. We don't have to worry about non-relational attributes, since Mirage doesn't really care for them. Finally, we create a new Mirage class with all the map relationships. And that's pretty much it. With a couple lines of code, we were able to cut down our testing setup from multiple days and weeks to a few minutes. All right, so now you might be wondering what do we do with all these different data models. We export all our data to Excel, and send out a weekly report to a stakeholder. No, just kidding. In Origin Story, we have a feature called Slates. Slates is a custom-build grid which allows our users to pick and choose different types of data to compare and contrast. Some features of Slates include occlusion rendering, fixed header and first column, custom editable sell elements, based on the different data types, and multiple layouts. To support a fixed header, Slates is currently built by using two Ember collections that share the same scroll handler. That way, when you horizontally scroll the content, the header scrolls along, and vice versa. Since the number of rows is more constant and much smaller than number of columns, our fixed left column doesn't need to be virtualized. To preserve the natural vertical scrolling, the fixed column must be rendered under the same scroll element as the main content, but with a fixed position. To support different row heights so that we can show as much data as possible to our users, each cell starts with a minimum height. When the content within changes, the cell dynamically adapts to all the inner content, sorry, so all the inner content is visible. It then sends an event to the parent row with its new height, and the layout gets recalculated. The tallest cell then determines the height of the row. Unfortunately, this is a fully handcrafted solution and took way too much dev time to achieve very common data grid tasks. In the Ember ecosystem today, we have these add-ons, among many more, but each with their own caveats. You have Ember Table, which is pre-Ember 2.x, you have Vertical Collection, which is slowly nearing its 1.0 release. You have Hands On Table, which is a third party library that's hard to deeply integrate with Ember. You have Horizontal Collection, which is yet to be built. And you have Ember Light Table, which has no inclusion rendering, but is in early development. To combat this, all the collabers have decided to come together. As Ember is maturing, and things like name blocks are being built out, we think that this is the right time to band together and create a single solution to meet all the community's use cases. All right, so what are we currently working towards? With the build system that Ember CLI provides, as well as the add-on ecosystem, our goal is to get Ember on a Netflix paved road. Solutions such as Bolt, Meechum with, Ember CLI Deploy, Ember Metrics, Ember CLI Mirage, and many more incredibly written add-ons, get us closer faster than we ever anticipated. We want to be able to create a blueprint so we can generate a production-ready Ember application right out of the box. We want this to be our blueprint to success. Thank you all very much.