The Engineering of a Design System

Our Implementation Experience at Traveloka

Raibima Putra
Traveloka Engineering Blog
13 min readMar 6, 2020

--

Despite a rising trend, most articles about design systems emphasized coverage on its design over its engineering from scalability preparation to change optimization.

This post is about how Traveloka prepared and scaled its design system for the web purely from an engineering point of view. After all, this is an engineering blog, right?! If you’re interested in learning more about design at Traveloka, make sure to check out our design blog.

Traveloka’s business is diversely extensive. At the time of this writing, we are operating in eight countries across Southeast Asia and Australia. We offer many different products related to travel, lifestyle, food, and financial services driven by our primary goal to empower users’ discoveries & enrich users’ journeys. In addition, our multicultural Engineering teams are spread across Jakarta, Singapore, as well as Bangalore.

Given the scale of our company, there’s definitely an urge to develop a solid design system for our products to sustain our trajectory ahead. Not only because we love our users, but we also love our people; the engineers, the designers, the product managers, etc being our most important asset, that we’d like them to fall into the pit of success. Moreover, we want to enable not just any collaboration, but one that is effective as well as productive.

Engineering a Design System

So some essential questions to ponder; What are the elements of design system engineering? What constitutes a great design system? What qualities should we aim for when shipping a design system?

Our answer to them is: it depends. Therefore, we think it’s best if we start by questioning the above questions to our stakeholders, the two biggest of which are the product engineers and the UI developers on what matters to them. In addition, it won’t hurt to do as much research as you can in order to obtain as many data points to help you narrow down on a design system specifications that best fit your organization’ needs.

Anyway, here’s what we think are the three paramount elements of design system engineering.

Design Specs

Obviously, you need a design spec in order to build a design system. We consider this element as the “first order” since no matter how good your preparation is, you still can’t build a design system without a design spec. So, the first step is to collaborate with designers, challenge them to be able to see design requirements and constraints that are distinct on each platform, tell them what’s possible and what’s not, as well as empower them with the context needed.

These are seven subjects that we believe are the foundation of a design spec:

  • Guidelines & principles
  • UI elements
  • Tokens
  • Colours
  • Icons
  • Typography
  • Accessibility

But, aren’t those components of a style guide? That’s actually a fair assessment. However, an effective design system is comprised of, not only a style guide, but also a solid tooling as well as a powerful governance model. As one of our community leaders, Dan Abramov, said the best designers “don’t stop at the first order”.

Tooling

When we set our goals for this project, we chose to adopt the following three attributes that we thought (and verified later) mattered the most for the majority of us:

Feedback loop

Everything should feel instant. When we modify something, we should immediately see its feedback. Slow feedback loop means poor developer experience, which leads to unhappy engineers.

Collaboration

At Traveloka, we have designers who code too. We call them UI Developers. They collaborate closely with web engineers to develop and maintain web UI components. Some web engineers are deeply skilled in Javascript but less so on other related domains such as HTML and CSS. So the presence of UI Developers greatly benefits our workflow.

Autonomy

Since we have so many teams working in different countries in multiple time zones, it only makes sense for us to be independent of each other while trusting the other person will pull through. We may also hold the same values, but our characteristics and workflows can be different. For instance, big teams such as Flights, tend to release more frequently than other smaller and younger teams, such as Trains. We don’t want our work to be blocked by other teams and vice versa.

There are also a lot of tools, which we use to support our design system that merit a blog of their own. However, in this post, we want to focus on the four that are the most relevant to our web engineers.

Tool #0: Typescript

We love JavaScript. It’s a mature, flexible, and very productive programming language. However, at scale, we have learnt that refactoring is a very common task for us and yet refactoring such a loosely-typed language can be super tedious and error-prone. Since your code is evaluated at runtime, there’s no way to tell if your code works after refactoring unless you run it. We usually mitigate this challenge by writing tests. But, we don’t normally assert types in our tests since it’s mostly implementation details which frequently change throughout the development lifecycle. To make matters worse, loosely-typed languages normally have poor IDE support since there’s no way for them to statically infer types and so, something as simple as changing a variable name could be marked unsafe.

Adopting Typescript makes a lot of sense to us. It’s essentially a superset of JavaScript, but with optional static typing support (it compiles to Javascript). It also allows us to incrementally migrate our UI code from JS because we don’t want to disrupt an ongoing feature development. Refactoring Typescript code is super easy since its IDE support is top notch. All we need to do is make changes to types & work on the errors shown in our IDE. Once they are all gone, our code can be run again as if nothing happened.

Typescript is also perfect for design systems for without it, referring to design tokens would be a back-and-forth nightmare since autocompletion for JS is not yet supported across IDE. Here’s what it looks like to develop a UI (under a design system) with Typescript:

Tool #1: React

React is an open source Javascript library that enables powerful componentization of web user interfaces. It is built and maintained by Facebook and adopted by many renowned companies around the world. The ecosystem is so huge that we feel confident about adopting it for this exact reason. However, in spite of our belief that React is definitely the first step towards enabling autonomy, we don’t want to turn this post into yet another framework war.

Tool #2: Storybook

Storybook is an open source tool for developing the so-called “dumb components” for UI. These components take data as “props” and render the UI according to the design specs. It doesn’t care about data fetching, routing, or anything that makes the actual app works. Instead, what it does verbally speaking is, “hey, just give me the data I need and I will figure out how it looks on the screen.” This makes collaboration between web engineers and UI developers easy and effective.

There are many reasons why we end up choosing Storybook for our UI development sandbox. One of the most compelling of which is the out of the box support for Hot Module Replacement (HMR). With HMR, any modification to our source code will be reflected instantly the moment we save it without the need to restart. As we have pointed out earlier, this is huge for us as it enables a super fast feedback loop! Furthermore, Storybook is very extensible with lots of community addons.

Despite those conveniences, Storybook does have its fair share of shortcomings. Let’s explore the issues we once faced one by one and go through how we were able to alleviate them almost completely.

Unnecessarily verbose & boilerplate-y

Note: the following pain point has been partially addressed in Storybook v5.2

We think that Storybook is pretty verbose & explicit when it comes to bootstrapping a new component. The way we showcase a component is through “stories” and here’s what it looks like to write a component & its stories in Storybook:

As you can see, it’s not the prettiest code in the world for a “hello world” component. We found out that when bootstrapping a new component, people tend to copy an existing stories file, modify it, and remove any unnecessary part. Too many ceremonies.

To overcome this problem, we created a component boilerplate generator script. The idea is that bootstrapping a new component and its stories should take no more than a single command. Here’s what the command looks like in action:

This command will generate roughly the same stories code as before.

Increasingly slow as the number of components grows

We have been using Storybook for almost two years now, even prior to the design system initiative. At that time, the tool itself was not as good as it is today and we didn’t even know if it would scale as the number of components grew. We started noticing significant slowdowns when the number of components hovered around 100 and it had gotten worse since then as the number kept climbing, impacting Storybook’s startup time and HMR. Working in it wasn’t as pleasant as it was first installed!

Then the investigation began. We identified that the way we configured our Storybook to load our component stories wasn’t really optimal. We used both regular expressions and require.context webpack API to traverse the file system to look for *.story.js and it seemed to have slowed down our compilation badly. We ended up duplicating our base storybook configuration file to mimic our folder structure — more on that later. Even though it increased the maintenance burden, we believed the trade-off was worth it and would lead to improvement in various areas, including compilation and startup times.

Again, a custom script for splitting workflows worked great for our needs. The idea is to avoid loading components that don’t matter to you and your workflow, like saying “hey, I don’t care about other team’s components. So, let’s just load ours”. Using just a single command, we were able to decouple a group of stories and components into separate workflows, making Storybook loads and recompiles faster by 60%! This approach also unlocks the possibility of build parallelization in our Continuous Integration (CI). Here’s what it looks like in practice:

To summarise what we’ve accomplished:

  • We were able to reduce the steps needed to bootstrap a UI element.
  • We were able to find a way to make Storybook more resilient to growth by splitting our compilation into groups.

Tool #3: React Native Web

In case you missed it, we use React Native in many places at Traveloka. But did you know that you can also use React Native to target the web?

The solution exists now in React Native Web (RNW). RNW makes code sharing between React Native and ReactDOM possible. It provides an almost identical set of APIs to React Native that leverage web APIs internally so that it can target browser environments. However, code sharing is not really the key reason we decided to go with it.

We feel like web apps still have a lot to catch up with native apps such as iOS and Android. By leveraging RNW, we were able to bring native app experience on par to our web experience since RNW handles cross-browser compatibility issues for us. Making components accessible is also much easier, thanks to rich APIs that RN & RNW provide.

We also think that RNW styling brings many benefits in the long run, including but not limited to:

  • Atomic CSS classes & deduplication, which reduces download size.
  • Deterministic style resolutions that give increasing precedence to more specific style declarations.
  • Easy static CSS extraction for above-the-fold content, which improves page’s loading times by only shipping just the right amount of CSS needed to render content visible on the first load.

If you are keen to learn more, the React Native for Web documentation is a great place to start.

Governance

Operating at scale requires much more than just solid tooling. Most of the time, it’s more about the people and less about the tools. Prior to the design system initiative, we didn’t have any form of governance that ruled over our UI codebase. As a result, people kept incorrectly adding stuff to it. Not only did this impede the discoverability of components and utilities, but it also created “zombie code”; undeletable code that has unclear ownership and usage. It made our codebase hard to maintain and scale.

However, as we have pointed out earlier, people love autonomy. They don’t want to work in a strict codebase nor do they want to be generalised in terms of workflows and habits. We realised that this was rather conflicting to what we set out to solve. Should we trade off autonomy for maintainability? Where should we draw the line between those two? Can’t we have both?

Turns out we can. Governance doesn’t always mean ruling. Instead, try to think of it as a way to make it clear about the ownership of something. Our recipe is simple: structure and no “shared”.

It’s important to note that although the following has worked well for us doesn’t mean that it will work for you. Your situation may be entirely different, so always check what matters to you and your organisation.

Structure

In preparation for the new design system, we decided to categorise UIs into 3 groups:

Core UI

The Core UI is maintained by design system implementers and it holds all UI components that have been standardised by the design team and agreed by the engineering team to be implemented for the web. Internally, we call it “Momentum” because that’s the name of our design system. Some examples of the Core UI components are button, text / typography, and card.

Future UI

Also maintained by design system implementers, sometimes, we have UI components that are common, but haven’t yet made it to the design system specs. We like to call them Future UI because we hope that they can get standardised soon. Plus, it’s cool to be able to use something from the “future”, right? At the time of this writing, some examples of Future UI components are modal and loading skeleton view. But by the time you’re reading this article, these Future UI components may have probably been promoted to Core UI.

Product UI

Product UI, maintained by the respective product domain, consists of product-specific UI components that are made from components in Core UI and/or Future UI. For instance, if you purchase a flight ticket from our website, the UIs that you interact with are likely the ones we put under the Product UI group. As we have many products, we organise this group by breaking it further into sub-groups where each sub-group is owned by a product domain.

Each product is free to implement their own governance because we believe that applying the same set of rules for everyone is just straight unproductive. Besides, most teams already have a tech lead, who can decide what’s best for their team and this separation of concern continues to work really well for us.

Picture derived from a Japanese anime series The Brave Fighter of Sun Fighbird

However, it’s important to note that having these separations doesn’t necessarily mean someone with the right credentials and access is not allowed to alter the code owned by another team. In fact, we encourage cross-contributions very much by utilizing many GitHub’s features like Issues for issues discoverability and Code Owners for facilitating pull request reviews.

No Shared

If you notice the above structure, there’s no such thing as “shared” or “common”. In fact, the very first thing we did was actually to get rid of them completely. As we have learnt over the years, “shared” folders encourage premature abstraction. People tend to put stuff under “shared” with no clear intention to keep or maintain it. As the number of dependent modules grows, the code gets more fragile, making it very difficult to refactor such as breaking some integration tests when a change is introduced. How often do you hear about the “XXX decoupling” project?

We encourage copy-and-paste vigorously lately to make patterns more apparent. We believe that abstractions should be built from patterns and not the other way around. As a pattern emerges, it signals a missing abstraction. This is where we usually perform what we call uplifting; a process, where we extract common patterns into a single abstraction, effectively removing code duplications.

In “This Is Not The DRY You Are Looking For”, Nicolò Pignatelli said “Duplication allows for delayed decisions. And that’s gold in software development. It’s ten times easier to refactor later from multiple specializations to a single abstraction than the other way around.”

What’s Ahead

While the foundation has been pretty much settled, we like to think that the work is far from over. We understand that product teams need to keep innovating. So we opted for an optional migration path, in which we don’t force product teams to immediately migrate to the new system. In fact, this is how we usually roll out big changes that potentially affect a bunch of components with one of the goals being to keep as few disruptions as possible to product development. However, we are actively advocating the new design system because we believe that our engineers and our users deserve a better experience from a better design system.

Engineering Traveloka’s design system has been a very challenging yet a joyful journey for us. Along the way, lots of people were involved, bugs were squashed, performance was squeezed, and processes were optimised. We think that it is now a fertile ground for contributions and you can definitely be part of it. If you’re interested, please do check out our career page.

--

--